import { Tag } from "./constants";
import { is, isAnnotated, IsPreservesAnnotated } from "./is";
import type { GenericEmbedded } from "./embedded";
import type { Value } from "./values";
import type { Encoder, Preservable } from "./encoder";
import type { Writer, PreserveWritable } from "./writer";

export interface Position {
    line?: number;
    column?: number;
    pos: number;
    name?: string;
}

export function newPosition(name?: string): Position {
    return { line: 1, column: 0, pos: 0, name };
}

export function updatePosition(p: Position, ch: string): boolean {
    p.pos++;
    if (p.line === void 0) {
        return false;
    } else {
        let advancedLine = false;
        switch (ch) {
            case '\t':
                p.column = (p.column! + 8) & ~7;
                break;
            case '\n':
                p.column = 0;
                p.line++;
                advancedLine = true;
                break;
            case '\r':
                p.column = 0;
                break;
            default:
                p.column!++;
                break;
        }
        return advancedLine;
    }
}

export function formatPosition(p: Position | null | string): string {
    if (p === null) {
        return '<unknown>';
    } else if (typeof p === 'string') {
        return p;
    } else {
        return `${p.name ?? ''}:${p.line ?? ''}:${p.column ?? ''}:${p.pos}`;
    }
}

export class Annotated<T = GenericEmbedded> implements Preservable<T>, PreserveWritable<T> {
    readonly annotations: Array<Value<T>>;
    readonly pos: Position | null;
    readonly item: Value<T>;

    constructor(item: Value<T>, pos?: Position) {
        this.annotations = [];
        this.pos = pos ?? null;
        this.item = item;
    }

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

    static __from_preserve__<T>(v: Value<T>): undefined | Annotated<T> {
        return isAnnotated<T>(v) ? v : void 0;
    }

    __preserve_on__(encoder: Encoder<T>): void {
        if (encoder.includeAnnotations) {
            for (const a of this.annotations) {
                encoder.state.emitbyte(Tag.Annotation);
                encoder.push(a);
            }
        }
        encoder.push(this.item);
    }

    __preserve_text_on__(w: Writer<T>): void {
        if (w.includeAnnotations) {
            const flat = this.annotations.length <= 1;
            for (const a of this.annotations) {
                w.state.pieces.push("@");
                w.push(a);
                if (flat) {
                    w.state.pieces.push(" ");
                } else {
                    w.state.writeIndentSpace();
                }
            }
        }
        w.push(this.item);
    }

    equals(other: any): boolean {
        return is(this.item, Annotated.isAnnotated(other) ? other.item : other);
    }

    // hashCode(): number {
    //     return hash(this.item);
    // }

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

    static isAnnotated<T = GenericEmbedded>(x: any): x is Annotated<T> {
        return isAnnotated(x);
    }
}

export function annotate<T = GenericEmbedded>(v0: Value<T>, ...anns: Value<T>[]): Annotated<T> {
    const v = Annotated.isAnnotated<T>(v0) ? v0 : new Annotated(v0);
    anns.forEach((a) => v.annotations.push(a));
    return v;
}

export function annotations<T = GenericEmbedded>(v: Value<T>): Array<Value<T>> {
    return Annotated.isAnnotated<T>(v) ? v.annotations : [];
}

export function position<T = GenericEmbedded>(v: Value<T>): Position | null {
    return Annotated.isAnnotated<T>(v) ? v.pos : null;
}
