/**
 * https://github.com/kawanet/git-cat-file
 */

import {promises as fs} from "fs"
import type {GCF} from "../types/git-cat-file.d.ts"
import {shortCache} from "./cache.ts"
import {Commit} from "./commit.ts"
import type {ObjStore} from "./obj-store.ts"
import {Tag} from "./tag.ts"

interface RefCommit {
    ref: string
    commit: string
}

const isObjectId = (oid: string) => (oid && /^[0-9a-f]{40}$/i.test(oid))

export class Ref {
    protected readonly root: string

    constructor(root: string) {
        this.root = root
    }

    async findCommitId(revision: string, store: ObjStore): Promise<string> {
        if (!revision || !/[^.\/]/.test(revision)) {
            throw new Error(`Invalid revision: ${revision}`)
        }

        let ancestry: string[]
        revision = revision.replace(/([~^]\d*)+$/, match => {
            ancestry = match.split(/([~^]\d*)/).filter(v => v)
            return ""
        })

        if (revision === "@") {
            revision = "HEAD"
        }

        const id = await this.findId(revision)
        if (id && !ancestry) return id

        const obj = await this.getRawCommit(id || revision, store)
        if (!obj) return
        if (obj && !ancestry) return obj.oid

        // https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection#_ancestry_references
        // HEAD^
        // HEAD~2
        let commit: GCF.Commit = new Commit(obj, store)
        for (const gen of ancestry) {
            const mark = gen[0]
            const num = gen.substring(1)

            const count = (num === "0") ? 0 : (+num || 1)
            const tilde = (mark === "~") ? count : 1
            const caret = (mark === "^") ? count : 1

            for (let i = 0; i < tilde; i++) {
                const parents = await commit.getParents()
                if (!parents) return
                if (caret) commit = parents[caret - 1]
                if (!commit) return
            }
        }

        return commit.getId()
    }

    private async findId(revision: string): Promise<string> {
        while (revision) {
            revision = await this.searchRef(revision)
            if (!revision) break

            const ref = revision.replace(/^ref:\s*/, "")
            if (/^refs\//.test(ref)) {
                revision = await this.findRef(ref)
            }

            if (isObjectId(revision)) {
                return revision // commit
            }
        }
    }

    private async getRawCommit(revision: string, store: ObjStore): Promise<GCF.IObject> {
        const object_id = await store.findObjectId(revision)
        if (!object_id) return // not found

        let obj = await store.getObject(object_id)

        if (obj?.type === "tag") {
            const tag = new Tag(obj, store)
            const object_id = tag.getMeta("object")
            obj = await store.getObject(object_id)
        }

        if (obj.type === "commit") return obj
    }

    private readPackedRefIndex = shortCache(async () => {
        const index: {[ref: string]: string} = {}
        const list = await this.readPackedRefList()
        for (const ref of list) {
            index[ref.ref] = ref.commit
        }
        return index
    })

    private async readPackedRefList(): Promise<RefCommit[]> {
        const list: RefCommit[] = []
        const text: string = await this.readTextFile(`packed-refs`).catch((): null => null)
        if (!text) return list

        const lines = text.split(/\r?\n/).filter(s => /^\w/.test(s))
        for (const line of lines) {
            const [commit, ref] = splitBySpace(line)
            list.push({commit, ref})
        }
        return list
    }

    private async searchRef(ref: string): Promise<string> {
        // .git/HEAD
        if (/HEAD$/.test(ref)) {
            const commit = await this.readFirstLine(ref).catch((): null => null)
            if (commit) return commit
        }

        // .git/refs/heads/main
        {
            const commit = await this.findRef(`refs/heads/${ref}`).catch((): null => null)
            if (commit) return commit
        }

        // .git/refs/tags/xxxx
        {
            const commit = await this.findRef(`refs/tags/${ref}`).catch((): null => null)
            if (commit) return commit
        }

        // .git/refs/remotes/xxxx
        {
            const commit = await this.findRef(`refs/remotes/${ref}`).catch((): null => null)
            if (commit) return commit
        }
    }

    private async findRef(name: string): Promise<string> {
        // loose ref
        const ref = await this.readFirstLine(name)
        if (ref) return ref

        // packed ref
        const index = await this.readPackedRefIndex()
        if (index[name]) return index[name]
    }

    private async readFirstLine(name: string): Promise<string> {
        const text: string = await this.readTextFile(name).catch((): null => null)
        if (text) return text.split(/\r?\n/).filter(s => /^[^#\s]/.test(s)).shift()
    }

    private readTextFile = shortCache((name: string): Promise<string> => {
        const path = `${this.root}/${name}`
        // console.warn(`readFile: ${path}`);
        return fs.readFile(path, "utf-8")
    })
}

function splitBySpace(line: string): string[] {
    const sp = line.indexOf(" ")
    return [line.slice(0, sp), line.slice(sp + 1)]
}
