#!/usr/bin/env python3
import sys
import os
import configparser
import struct
import zlib
import hashlib


def cmd_version():
    print("git version 2.39.0")


def find_git_dir():
    d = os.getcwd()
    while d != "/":
        g = os.path.join(d, ".git")
        if os.path.isdir(g):
            return g
        d = os.path.dirname(d)
    return None


def read_loose_object(git_dir, sha):
    path = os.path.join(git_dir, "objects", sha[:2], sha[2:])
    if not os.path.exists(path):
        return None
    with open(path, "rb") as f:
        raw = zlib.decompress(f.read())
    hdr, _, body = raw.partition(b"\x00")
    otype, size = hdr.decode().split(" ")
    return otype, int(size), body


def read_pack_object(git_dir, sha):
    pack_dir = os.path.join(git_dir, "objects", "pack")
    if not os.path.isdir(pack_dir):
        return None
    sha_bin = bytes.fromhex(sha)
    for f in os.listdir(pack_dir):
        if f.endswith(".idx"):
            idx_path = os.path.join(pack_dir, f)
            pack_path = idx_path[:-4] + ".pack"
            offset = lookup_idx(idx_path, sha_bin)
            if offset is not None:
                return read_pack_entry(pack_path, offset)
    return None


def lookup_idx(idx_path, sha_bin):
    with open(idx_path, "rb") as f:
        magic = f.read(4)
        if magic != b"\xfftOc":
            return None
        f.read(4)
        fanout = struct.unpack(">256I", f.read(256 * 4))
        total = fanout[255]
        first_byte = sha_bin[0]
        start = fanout[first_byte - 1] if first_byte > 0 else 0
        end = fanout[first_byte]
        sha_table_offset = 8 + 256 * 4
        for i in range(start, end):
            f.seek(sha_table_offset + i * 20)
            entry_sha = f.read(20)
            if entry_sha == sha_bin:
                crc_offset = sha_table_offset + total * 20
                offset32_offset = crc_offset + total * 4
                f.seek(offset32_offset + i * 4)
                off = struct.unpack(">I", f.read(4))[0]
                if off & 0x80000000:
                    offset64_offset = offset32_offset + total * 4
                    f.seek(offset64_offset + (off & 0x7FFFFFFF) * 8)
                    off = struct.unpack(">Q", f.read(8))[0]
                return off
    return None


def read_pack_entry(pack_path, offset):
    OBJ_TYPES = {1: "commit", 2: "tree", 3: "blob", 4: "tag"}
    with open(pack_path, "rb") as f:
        f.seek(offset)
        byte = f.read(1)[0]
        obj_type = (byte >> 4) & 0x07
        size = byte & 0x0F
        shift = 4
        while byte & 0x80:
            byte = f.read(1)[0]
            size |= (byte & 0x7F) << shift
            shift += 7

        if obj_type == 6:
            byte = f.read(1)[0]
            base_offset = byte & 0x7F
            while byte & 0x80:
                byte = f.read(1)[0]
                base_offset = ((base_offset + 1) << 7) | (byte & 0x7F)
            base_abs = offset - base_offset
            compressed = f.read(size + 512)
            try:
                delta = zlib.decompress(compressed)
            except Exception:
                delta = zlib.decompress(compressed, -15)
            base = read_pack_entry(pack_path, base_abs)
            if base is None:
                return None
            result = apply_delta(base[2], delta)
            return base[0], len(result), result

        if obj_type == 7:
            base_sha = f.read(20).hex()
            compressed = f.read(size + 512)
            try:
                delta = zlib.decompress(compressed)
            except Exception:
                delta = zlib.decompress(compressed, -15)
            git_dir = os.path.dirname(os.path.dirname(pack_path))
            base = resolve_object(git_dir, base_sha)
            if base is None:
                return None
            result = apply_delta(base[2], delta)
            return base[0], len(result), result

        compressed_start = f.tell()
        rest = f.read(max(size * 2, 4096))
        try:
            body = zlib.decompress(rest)
        except Exception:
            body = zlib.decompress(rest, -15)
        type_name = OBJ_TYPES.get(obj_type, "unknown")
        return type_name, len(body), body


def apply_delta(base, delta):
    pos = 0

    def read_size():
        nonlocal pos
        s = 0
        shift = 0
        while True:
            b = delta[pos]
            pos += 1
            s |= (b & 0x7F) << shift
            shift += 7
            if not (b & 0x80):
                break
        return s

    read_size()
    read_size()
    result = bytearray()
    while pos < len(delta):
        cmd = delta[pos]
        pos += 1
        if cmd & 0x80:
            cp_off = 0
            cp_size = 0
            if cmd & 0x01:
                cp_off = delta[pos]; pos += 1
            if cmd & 0x02:
                cp_off |= delta[pos] << 8; pos += 1
            if cmd & 0x04:
                cp_off |= delta[pos] << 16; pos += 1
            if cmd & 0x08:
                cp_off |= delta[pos] << 24; pos += 1
            if cmd & 0x10:
                cp_size = delta[pos]; pos += 1
            if cmd & 0x20:
                cp_size |= delta[pos] << 8; pos += 1
            if cmd & 0x40:
                cp_size |= delta[pos] << 16; pos += 1
            if cp_size == 0:
                cp_size = 0x10000
            result.extend(base[cp_off:cp_off + cp_size])
        elif cmd > 0:
            result.extend(delta[pos:pos + cmd])
            pos += cmd
    return bytes(result)


def resolve_object(git_dir, sha):
    obj = read_loose_object(git_dir, sha)
    if obj:
        return obj
    return read_pack_object(git_dir, sha)


def cmd_cat_file_batch(check_only=False):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(1)
    while True:
        line = sys.stdin.readline()
        if not line:
            break
        sha = line.strip()
        if not sha:
            continue
        sha = resolve_ref(git_dir, sha) or sha
        obj = resolve_object(git_dir, sha)
        if obj is None:
            sys.stdout.write(f"{sha} missing\n")
            sys.stdout.flush()
        else:
            otype, size, body = obj
            if check_only:
                sys.stdout.write(f"{sha} {otype} {size}\n")
                sys.stdout.flush()
            else:
                sys.stdout.write(f"{sha} {otype} {size}\n")
                sys.stdout.flush()
                sys.stdout.buffer.write(body)
                sys.stdout.buffer.write(b"\n")
                sys.stdout.buffer.flush()


def resolve_ref(git_dir, ref):
    if len(ref) == 40 and all(c in "0123456789abcdef" for c in ref):
        return ref
    ref_path = os.path.join(git_dir, ref)
    if os.path.isfile(ref_path):
        content = open(ref_path).read().strip()
        if content.startswith("ref: "):
            return resolve_ref(git_dir, content[5:])
        return content
    packed = os.path.join(git_dir, "packed-refs")
    if os.path.isfile(packed):
        for pline in open(packed):
            pline = pline.strip()
            if pline.startswith("#") or pline.startswith("^"):
                continue
            parts = pline.split(" ", 1)
            if len(parts) == 2 and parts[1] == ref:
                return parts[0]
    return None


def cmd_rev_parse(args):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(1)
    for arg in args:
        if arg == "--git-dir":
            print(git_dir)
        elif arg == "--show-toplevel":
            print(os.path.dirname(git_dir))
        elif arg == "--is-inside-work-tree":
            print("true")
        elif arg == "--show-cdup":
            print("")
        elif arg == "--absolute-git-dir":
            print(os.path.abspath(git_dir))
        else:
            sha = resolve_ref(git_dir, arg)
            if sha is None:
                sha = resolve_ref(git_dir, f"refs/heads/{arg}")
            if sha is None:
                sha = resolve_ref(git_dir, f"refs/tags/{arg}")
            if sha:
                print(sha)
            else:
                print(arg)


def cmd_symbolic_ref(args):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(1)
    head = open(os.path.join(git_dir, "HEAD")).read().strip()
    if head.startswith("ref: "):
        print(head[5:])
    else:
        sys.exit(1)


def cmd_config(args):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(128)
    cfg = configparser.ConfigParser()
    cfg.read(os.path.join(git_dir, "config"))
    if "-l" in args or "--list" in args:
        for section in cfg.sections():
            for key, val in cfg.items(section):
                print(f"{section}.{key}={val}")
    elif "--get" in args or len(args) >= 1:
        key = args[-1]
        parts = key.rsplit(".", 1)
        if len(parts) == 2:
            try:
                print(cfg.get(parts[0], parts[1]))
            except (configparser.NoSectionError, configparser.NoOptionError):
                sys.exit(1)


def cmd_for_each_ref(args):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(1)
    fmt = "%(objectname) %(objecttype) %(refname)"
    prefix = ""
    for i, a in enumerate(args):
        if a.startswith("--format="):
            fmt = a.split("=", 1)[1]
        elif not a.startswith("-"):
            prefix = a
    refs_dir = os.path.join(git_dir, "refs")
    for root, dirs, files in os.walk(refs_dir):
        for fname in files:
            ref_path = os.path.join(root, fname)
            refname = os.path.relpath(ref_path, git_dir)
            if prefix and not refname.startswith(prefix):
                continue
            sha = open(ref_path).read().strip()
            line = fmt.replace("%(objectname)", sha)
            line = line.replace("%(refname)", refname)
            line = line.replace("%(refname:short)", fname)
            print(line)
    packed = os.path.join(git_dir, "packed-refs")
    if os.path.isfile(packed):
        for pline in open(packed):
            pline = pline.strip()
            if pline.startswith("#") or pline.startswith("^"):
                continue
            parts = pline.split(" ", 1)
            if len(parts) == 2:
                sha, refname = parts
                if prefix and not refname.startswith(prefix):
                    continue
                line = fmt.replace("%(objectname)", sha)
                line = line.replace("%(refname)", refname)
                line = line.replace("%(refname:short)", refname.rsplit("/", 1)[-1])
                print(line)


def cmd_diff(args):
    pass


def cmd_log(args):
    git_dir = find_git_dir()
    if not git_dir:
        sys.exit(1)
    head_sha = resolve_ref(git_dir, "HEAD")
    if head_sha:
        obj = resolve_object(git_dir, head_sha)
        if obj:
            sys.stdout.buffer.write(obj[2])
            sys.stdout.buffer.write(b"\n")


def cmd_checkout(args):
    pass


def cmd_status(args):
    print("On branch main")
    print("nothing to commit, working tree clean")


def main():
    if len(sys.argv) < 2:
        cmd_version()
        return

    cmd = sys.argv[1]
    args = sys.argv[2:]

    if cmd == "version":
        cmd_version()
    elif cmd == "cat-file":
        check = "--batch-check" in args
        cmd_cat_file_batch(check_only=check)
    elif cmd == "rev-parse":
        cmd_rev_parse(args)
    elif cmd == "symbolic-ref":
        cmd_symbolic_ref(args)
    elif cmd == "config":
        cmd_config(args)
    elif cmd == "for-each-ref":
        cmd_for_each_ref(args)
    elif cmd == "diff":
        cmd_diff(args)
    elif cmd == "log":
        cmd_log(args)
    elif cmd == "checkout":
        cmd_checkout(args)
    elif cmd == "status":
        cmd_status(args)
    else:
        pass


if __name__ == "__main__":
    main()
