#!/usr/bin/env python3

# =============================================================================
# krissz2rnd.py
# -----------------------------------------------------------------------------
# convert Krissz BD cave files to R'n'D level sets
# =============================================================================

from bs4 import BeautifulSoup

import json 
import os
import sys
import re
import shutil

kre_object_map = {
    'I': ' ',
    'H': '.',
    'A': 'r',
    '1': '%',
    '2': '!',
    'B': 'd',
    'E': 'W',
    'D': 'w',
    'C': 'M',
    'F': 'x',
    'T': 'v',
    'U': 'e',
    'V': 'o',
    'J': 'q',
    'W': 'Q',
    'X': 'O',
    'Y': 'b',
    'K': 'c',
    'Z': 'C',
    '0': 'B',
    'L': 'a',
    'N': 's',
    'S': 'F',
    'G': 'P',
    'M': 'X',
    'R': 'H'
}

perm_map = {0: 0, 1: 16, 2: 24, 3: 56, 4: 60, 5: 124, 6: 126, 7: 254, 8: 255}


# -----------------------------------------------------------------------------
# functions
# -----------------------------------------------------------------------------

def decompress(compressed):
    """Decompress a list of output ks to a string."""

    # Build the dictionary.
    dict_size = 256
    dictionary = {i: chr(i) for i in range(dict_size)}

    w = result = chr(compressed.pop(0))
    for k in compressed:
        if k in dictionary:
            entry = dictionary[k]
        elif k == dict_size:
            entry = w + w[0]
        else:
            #entry = k
            raise ValueError('Bad compressed k: %s' % k)
        result += entry

        # Add w+entry[0] to the dictionary.
        dictionary[dict_size] = w + entry[0]
        dict_size += 1

        w = entry
    return result

def get_normalized_cave_name(cave_name):
    cave_name_length = len(cave_name)
    cave_name = cave_name.lower()
    cave_name = re.sub('[^a-z0-9]+', '-', cave_name)
    cave_name = re.sub('^-', '', cave_name)
    cave_name = re.sub('-$', '', cave_name)

    if cave_name == "":
        cave_name = "000-invalid-cave-name-" + f"{cave_name_length:03d}"

    return cave_name

def rename_file(filename_current, cave_basename, cave_code, cave_author, has_demo, engine_type):
    match = re.search(r"^(.*)/([^/]+)\.([a-z]+)$", filename_current)
    if not match:
        print(f"ERROR: Cannot parse file '{filename_current}'!\n")
        exit(10)

    directory = match.group(1)
    basename  = match.group(2)
    extension = match.group(3)

    cave_author = cave_author.lower()
    cave_author = re.sub('[^a-z0-9]+', '-', cave_author)

    filename_new = directory + "/" + cave_basename + "." + cave_author
    filename_new += "." + engine_type
    filename_new += "." + ("demo" if has_demo else "cave")
    filename_new += "." + cave_code + "." + extension

    if filename_new != filename_current:
        if os.path.exists(filename_new):
            print(f"ERROR: Refusing to rename file '{filename_current}' to already existing file '{filename_new}'!\n")
            exit(10)

        print("::: RENAME '" + filename_current + "' => '" + filename_new + "'")
        os.rename(filename_current, filename_new)

def add_message_file(cave_code, level_id, args):
    src_filename = args["message_dir"] + "/" + cave_code + ".txt"
    dst_filename = args["dest_dir"] + "/" + f"{level_id:03d}" + ".txt"

    if os.path.exists(src_filename):
        shutil.copyfile(src_filename, dst_filename)
        print(f"  {args["prefix"]}Adding message file for level {level_id:03d} ...")

def convert_old_engine(input_file, f, level_id, args):
    with open(input_file, "r" ) as inp:
        raw_data = inp.read()
        soup = BeautifulSoup(raw_data, 'html.parser')
        cave_data = soup.find('div', id='caveProperties')
        cave_name = cave_data["data-cavename"]
        cave_code = cave_data["data-code"]
        cave_basename = get_normalized_cave_name(cave_name)

        caveName = cave_data["data-cavename"] if not args["capitalize"] else cave_data["data-cavename"].upper()
        amoebaSlowGrowthTime = cave_data["data-amoebaslowgrowthtime"]
        bonusDiamondValue = cave_data["data-bonusdiamondvalue"]
        diamondValue = cave_data["data-diamondvalue"]
        gameSpeedFps = cave_data["data-fps"]
        magicWallTime = cave_data["data-magicwalltime"]
        requiredDiamonds = cave_data["data-requireddiamonds"]
        slimePermeability = cave_data["data-slimepermeability"]
        timeLimit = cave_data["data-timelimit"]
        caveWidth = 40 # all of them?
        caveHeight = 22 # all of them?
        color1 = "#" + cave_data["data-color1"]
        color2 = "#" + cave_data["data-color2"]
        color3 = "#" + cave_data["data-color3"]
        color4 = "#" + cave_data["data-color4"]
        objectMap = cave_data["data-cavedata"]
        amoebaThreshold = 0.22727
        frameTime = round(1000.0 / float(gameSpeedFps))

        # cave-specific info for cave conversion
        cave_author_name = "Unknown"
        if raw_data.find(">by ") != -1:
            cave_author_name = raw_data.split(">by ")[1].split("<")[0]

        # demo-specific info for tape conversion
        demo_player_name = ""
        if raw_data.find(">demo by ") != -1:
            demo_player_name = raw_data.split(">demo by ")[1].split("</span>")[0]
        if demo_player_name == "":
            if str(input_file).find(" demo by ") != -1:
                demo_player_name = str(input_file).split(" demo by ")[1].split(").")[0]
        demo_data_steps = cave_data["data-demosteps"] if raw_data.find("data-demosteps") != -1 or raw_data.find("data-demoSteps") != -1 else ""
        # BDCFF uses space as the separator, so convert comma into space for storing
        demo_data_randoms = cave_data["data-demorandoms"].replace(",", " ") if raw_data.find("data-demorandoms") != -1 or raw_data.find("data-demoRandoms") != -1 else ""

        has_demo = (demo_data_steps != "")

        idx = 0
        cave_map = ""
        for y in range(int(caveHeight)):
            for x in range(int(caveWidth)):
                cave_map += kre_object_map[objectMap[idx]]
                idx += 1
            cave_map += "\n"

    if args["dump_data"]:
        # print(cave_code + " 0000-00-00 old - " + cave_basename + " " + input_file)
        print(cave_code + " " + cave_basename + " " + cave_author_name + " " + input_file)
        print(cave_data)

    if args["rename_files"]:
        rename_file(input_file, cave_basename, cave_code, cave_author_name, has_demo, "old")

    if args["skip_old_demos"]:
        has_demo = False

    f.write("[cave]\n")
    f.write(f"Name={caveName}\n")        
    f.write(f"Author={cave_author_name}\n")
    f.write("Intermission=false\n")        
    f.write(f"CaveTime={int(timeLimit)}\n")        
    f.write(f"DiamondsRequired={requiredDiamonds}\n")
    f.write(f"DiamondValue={diamondValue} {bonusDiamondValue}\n")
    f.write(f"AmoebaTime={amoebaSlowGrowthTime}\n")
    f.write(f"MagicWallTime={magicWallTime}\n")
    f.write("BorderProperties.lineshift=false\n")
    f.write(f"Size={caveWidth} {caveHeight}\n")
    f.write(f"FrameTime={frameTime}\n")
    f.write(f"MagicWallProperties.convertamoeba=false\n")
    f.write(f"AmoebaThreshold={amoebaThreshold:.5f}\n")
    f.write(f"HatchingDelay=20\n")  # FIXME: +1 or not? Default 21 in GDash, 20 in Krissz
    f.write(f"SlimePermeabilityC64={perm_map[int(slimePermeability)]}\n")
    f.write(f"Colors=#000000 {color1} {color4} {color3} {color2} {color2} {color2}\n")        
    f.write(f"KrisszEngineCaveCode={cave_code}\n")
    f.write(f"KrisszEngineNewEngine=false\n")
    f.write("\n")

    # convert the demo into a BDCFF replay if possible
    # TODO: finish the implementation, randoms are not implemented yet + properties like Success, Checksum, etc. are stubs!
    if has_demo:
        f.write("[replay]\n")
        f.write(f"Player={demo_player_name}\n")
        #f.write(f"Date={2025}\n") # TODO: stub! No way to determine the date from KrE (stored only in the server database).
        f.write(f"RecordedWith=OKrE\n")
        f.write(f"RandomSeed=-1\n") # TODO: stub! Not used by KrE, since the engine stores results of getRandom calls directly in "randoms"
        f.write(f"Score=0\n") # TODO: stub! No way to identify directly.
        f.write(f"Success=true\n") # TODO: assumed success, KrE only allows to store successful demos.
        #f.write(f"Checksum=0\n") # TODO: stub!
        # Convert movements from demo_data_steps
        movements = ""
        for step in demo_data_steps.split(","):
            if step == "-":
                movements += ". "
            elif step == "ln":
                movements += "l "
            elif step == "rn":
                movements += "r "
            elif step == "un":
                movements += "u "
            elif step == "dn":
                movements += "d "
            elif step == "ly":
                movements += "L "
            elif step == "ry":
                movements += "R "
            elif step == "uy":
                movements += "U "
            elif step == "dy":
                movements += "D "
            else:
                print(f"Warning: unknown step in demo data: {step}\n")
        movements = movements.strip()
        # pack the movements into the <motion><count> format
        packed_movements = ""
        cur_move = ""
        cur_count = -1
        for move in movements.split():
            if cur_move != move:
                if cur_count != -1:
                    if cur_count > 1:
                        packed_movements += f"{cur_move}{cur_count} "
                    else:
                        packed_movements += f"{cur_move} "
                cur_move = move
                cur_count = 1
            else:
                cur_count += 1
        if cur_count > 1:
            packed_movements += f"{cur_move}{cur_count} "
        else:
            packed_movements += f"{cur_move} "
        f.write(f"Movements={packed_movements}\n")
        f.write(f"Randoms={demo_data_randoms}\n")
        f.write("[/replay]\n\n")

    f.write("[map]\n")
    f.write(cave_map)
    f.write("[/map]\n")
    f.write("[/cave]\n")
    f.write("\n")

    if args["message_dir"] != "":
        add_message_file(cave_code, level_id, args)
        
def convert_new_engine(input_file, f, level_id, args):
    _CR = [] # cave data
    _DR = [] # demo data (tape)
    cave_author_name = "Unknown"
    demo_player_name = "Unknown"
    with open(input_file, "r") as inp:
        lines = inp.readlines()
        for line in lines:
            if line.find("_CR") != -1 and len(_CR) == 0:
                # load cave data
                _CR_raw = line.split("_CR=[")[1].split("];")[0].split(",")
                for code in _CR_raw:
                    _CR.append(int(code))
            if line.find("_DR") != -1 and len(_DR) == 0:
                # load demo data, if available
                _DR_raw = line.split("_DR=[")[1].split("];")[0].split(",")
                if _DR_raw[0] != '':
                    for code in _DR_raw:
                        _DR.append(int(code))
            if line.find(">by ") != -1:
                cave_author_name = line.split(">by ")[1].split("<")[0]
            if line.find(">demo by ") != -1:
                demo_player_name = line.split(">demo by ")[1].split("</span>")[0]
    
    # decompress the cave data
    cave_data = json.loads(decompress(_CR))
    cave_name = cave_data["caveName"]
    cave_code = cave_data["code"]
    cave_basename = get_normalized_cave_name(cave_name)

    demo_data = None
    # decompress the demo data, if available
    if len(_DR) > 0:
        demo_data = json.loads(decompress(_DR))
        if demo_player_name == "":
            if demo_data.has_key("creatorName"):
                demo_player_name = demo_data["creatorName"]
            elif str(input_file).find(" demo by ") != -1:
                demo_player_name = str(input_file).split(" demo by ")[1].split(").")[0]
        demo_data["randoms"] = demo_data["randoms"].replace(",", " ") # BDCFF stores values with the space as the separator

    has_demo = (demo_data != None)

    caveName = cave_data["caveName"] if not args["capitalize"] else cave_data["caveName"].upper()
    creatorName = cave_data["creatorName"]
    createTime = cave_data["createTime"].split(" ")[0]
    modifyTime = cave_data["modifyTime"].split(" ")[0]
    timeLimit = cave_data["timeLimit"]
    valueOfASecond = cave_data["timeValue"]
    rockfordBirthTime = cave_data["birthTime"]
    requiredDiamonds = cave_data["requiredDiamonds"]
    diamondValue = cave_data["diamondValue"]
    bonusDiamondValue = cave_data["bonusDiamondValue"]
    gameSpeedFps = cave_data["fps"]
    amoebaSlowGrowthTime = cave_data["amoebaSlowGrowthTime"]
    amoebaLimit = cave_data["amoebaLimit"]
    magicWallTime = cave_data["magicWallTime"]
    magicWallStopsAmoeba = cave_data["magicWallStopsAmoeba"]
    slimePermeability = cave_data["slimePermeability"]
    caveWidth = cave_data["width"]
    caveHeight = cave_data["height"]
    caveOpenBorder = cave_data["openBorder"]
    infiniteScroll = cave_data["infiniteScroll"]
    color1 = "#" + cave_data["color1"]
    color2 = "#" + cave_data["color2"]
    color3 = "#" + cave_data["color3"]
    color4 = "#" + cave_data["color4"]
    objectMap = cave_data["caveData"]
    amoebaThreshold = float(amoebaLimit) / (int(caveWidth) * int(caveHeight))
    if amoebaThreshold > 1.0:
        amoebaThreshold = 1.0
    frameTime = round(1000.0 / float(gameSpeedFps))
    
    if cave_author_name == "Unknown":
        cave_author_name = creatorName

    createYear = createTime.split("-")[0]
    if args["year"] == "Unknown" or createYear > args["year"]:
        args["year"] = createYear

    idx = 0
    cave_map = ""
    for y in range(int(caveHeight)):
        for x in range(int(caveWidth)):
            cave_map += kre_object_map[objectMap[idx]]
            idx += 1
        cave_map += "\n"

    has_magic_wall = 'M' in cave_map
    has_heavy_boulders = '%' in cave_map
    has_light_boulders = '!' in cave_map
    has_slime = 's' in cave_map

    open_border_h = "true" if "h" in caveOpenBorder else "false"
    open_border_v = "true" if "v" in caveOpenBorder else "false"
    inf_scrolling = "true" if infiniteScroll["h"] != False or infiniteScroll["v"] != False else "false"

    # reverse time
    if timeLimit == "-1":
        timeLimit = 0 # reverse time caves use the value 0 in RnD. TODO: if KrE style reverse time is differentiated from GDash style reverse time, add the relevant option here.
    # infinite time
    no_time = timeLimit == "0"

    if args["dump_data"]:
        # print(cave_code + " " + cave_data["createTime"] + " new " + cave_data["creatorName"] + " " + cave_basename + " " + input_file)
        print(cave_code + " " + cave_basename + " " + cave_author_name + " " + input_file)
        print(cave_data)

    if args["rename_files"]:
        rename_file(input_file, cave_basename, cave_code, cave_author_name, has_demo, "new")

    f.write("[cave]\n")
    f.write(f"Name={caveName}\n")        
    f.write(f"Author={cave_author_name}\n")
    f.write(f"Date={createTime}\n")
    f.write("Intermission=false\n")        
    f.write(f"CaveTime={int(timeLimit)}\n")        
    if no_time:
        f.write(f"NoTime=true\n")
    f.write(f"TimeValue={valueOfASecond}\n")
    f.write(f"DiamondsRequired={requiredDiamonds}\n")
    f.write(f"DiamondValue={diamondValue} {bonusDiamondValue}\n")
    f.write(f"AmoebaTime={amoebaSlowGrowthTime}\n")
    f.write(f"AmoebaThreshold={amoebaThreshold:.5f}\n")
    f.write(f"HatchingDelay={int(rockfordBirthTime)}\n")
    f.write(f"MagicWallTime={magicWallTime}\n")
    if has_magic_wall and has_heavy_boulders:
        f.write(f"MagicWallProperties.megastoneto=SCANN_DIAMONDf\n")
    if has_magic_wall and has_light_boulders:
        f.write(f"MagicWallProperties.lightstoneto=SCANN_DIAMONDf\n")
    f.write("BorderProperties.lineshift=false\n")
    f.write(f"Size={caveWidth} {caveHeight} 0 0 {int(caveWidth)-1} {int(caveHeight)-1}\n")
    f.write(f"FrameTime={frameTime}\n")
    f.write(f"MagicWallProperties.convertamoeba={'true' if magicWallStopsAmoeba == 'y' else 'false'}\n")
    if has_slime:
        f.write(f"SlimeProperties=DIAMOND SCANN_DIAMONDf BOULDER SCANN_BOULDERf MEGABOULDER SCANN_MEGABOULDERf LIGHTBOULDER SCANN_LIGHTBOULDERf\n")
    f.write(f"SlimePermeabilityC64={perm_map[int(slimePermeability)]}\n")
    f.write(f"BorderProperties.openbordershorizontal={open_border_h}\n")
    f.write(f"BorderProperties.openbordersvertical={open_border_v}\n")
    f.write(f"BorderProperties.infinitescrolling={inf_scrolling}\n")
    f.write(f"Colors=#000000 {color1} {color4} {color3} {color2} {color2} {color2}\n")
    f.write(f"KrisszEngineCaveCode={cave_code}\n")
    f.write(f"KrisszEngineNewEngine=true\n")
    f.write("\n")

    # convert the demo into a BDCFF replay if possible
    # TODO: finish the implementation, randoms are not implemented yet + properties like Success, Checksum, etc. are stubs!
    if has_demo:
        f.write("[replay]\n")
        f.write(f"Player={demo_player_name}\n")
        #f.write(f"Date={2025}\n") # TODO: stub! No way to determine the date from KrE (stored only in the server database).
        f.write(f"RecordedWith=NKrE\n")
        f.write(f"RandomSeed=-1\n") # TODO: stub! Not used by KrE, since the engine stores results of getRandom calls directly in "randoms"/
        f.write(f"Score=0\n") # TODO: stub! No way to identify directly.
        f.write(f"Success=true\n") # TODO: assumed success, KrE only allows to store successful demos.
        #f.write(f"Checksum=0\n") # TODO: stub!
        # Convert movements from demo_data_steps
        movements = ""
        for step in demo_data["steps"].split(","):
            if step == "-":
                movements += ". "
            elif step == "ln":
                movements += "l "
            elif step == "rn":
                movements += "r "
            elif step == "un":
                movements += "u "
            elif step == "dn":
                movements += "d "
            elif step == "ly":
                movements += "L "
            elif step == "ry":
                movements += "R "
            elif step == "uy":
                movements += "U "
            elif step == "dy":
                movements += "D "
            else:
                print(f"Warning: unknown step in demo data: {step}\n")
        movements = movements.strip()
        # pack the movements into the <motion><count> format
        packed_movements = ""
        cur_move = ""
        cur_count = -1
        for move in movements.split():
            if cur_move != move:
                if cur_count != -1:
                    if cur_count > 1:
                        packed_movements += f"{cur_move}{cur_count} "
                    else:
                        packed_movements += f"{cur_move} "
                cur_move = move
                cur_count = 1
            else:
                cur_count += 1
        if cur_count > 1:
            packed_movements += f"{cur_move}{cur_count} "
        else:
            packed_movements += f"{cur_move} "
        f.write(f"Movements={packed_movements}\n")
        f.write(f"Randoms={demo_data["randoms"]}\n")
        f.write("[/replay]\n\n")

    f.write("[map]\n")
    f.write(cave_map)
    f.write("[/map]\n")
    f.write("[/cave]\n")
    f.write("\n")

    if args["message_dir"] != "":
        add_message_file(cave_code, level_id, args)

    return args["year"]

def get_sorted_file_list(src_dir, sort_files):

    # read all cave filenames
    filelist = os.listdir(src_dir)
    filelist_sorted = {}

    # only return unsorted file list, if requested
    if not sort_files:
        for basename in filelist:
            # force file names like "42 b" being listed after "42"
            sortname = basename.replace(".", " ")
            filelist_sorted[sortname] = basename

        return filelist_sorted

    # sort all cave filenames
    for basename in filelist:
        # force file names like "42 b" being listed after "42"
        basename_part = basename.replace(".", " ")
        sort_prefix = ""

        # cut off cave code and file extension (not used for sorting)
        match = re.search(r"^(.*)\.[A-Za-z0-9]{12}\.[a-z]+$", basename_part)
        if match:
            basename_part = match.group(1)

        # cut off last part of filename with username (not used for sorting)
        match = re.search(r"^(.* )\(by [A-Za-z0-9]+\).*", basename_part)
        if match:
            basename_part = match.group(1)

        # check for special matches (like "am22-01.2-something")
        match = re.search(r"^am[0-9][0-9]-(..)-([0-9])-(.*)", basename_part)
        if match:
            sort_prefix += "000" + match.group(1) + match.group(2) + " - "
            basename_part = match.group(3)

        # check for special matches (like "am22-everything-else")
        match = re.search(r"^am[0-9][0-9]-(...)(.*)", basename_part)
        if match:
            sort_prefix += "000" + match.group(1) + " - "
            basename_part = match.group(2)

        # check for special matches
        match = re.search(r"^stripes-dx-(..)(.*)", basename_part)
        if match:
            sort_prefix += "0000" + match.group(1) + " - "
            basename_part = match.group(2)

        # check for special matches
        match = re.search(r"^boulder-dash-for-newcomers-b([0-9])(.*)", basename_part)
        if match:
            sort_prefix += "99999" + match.group(1) + " - "
            basename_part = match.group(2)

        # check for numbers (three times)
        for i in range(3):
            number = 0
            match = re.search(r"^[^0-9]*([0-9]+)(.*)", basename_part)
            if match:
                number = int(match.group(1))
                basename_part = match.group(2)

            sort_prefix += f"{number:06d}" + " - "

        sortname = sort_prefix + basename_part + " - " + basename
        filelist_sorted[sortname] = basename

    for sortname in sorted(filelist_sorted.keys()):
        basename = filelist_sorted[sortname]
        # print("::: " + sortname + " => " + basename)

    return filelist_sorted

def convert_set(src_dir, dest_dir, set_name, auth_name, args):
    num_levels = 0
    if not os.path.exists(dest_dir):
        os.mkdir(dest_dir)

    bdcff_basename = re.sub('[^A-Za-z0-9]+', '_', set_name) + ".bd"

    # write the BDCFF header
    out_bdcff = open(os.path.join(dest_dir, bdcff_basename), "w")
    out_bdcff.write("; converted using KrisszConvert by Michael Kamensky\n")
    out_bdcff.write("[BDCFF]\n")
    out_bdcff.write("Version=0.5\n")
    out_bdcff.write("\n")
    out_bdcff.write("[mapcodes]\n")
    out_bdcff.write("Length=1\n")
    out_bdcff.write("%=MEGABOULDER\n")
    out_bdcff.write("!=LIGHTBOULDER\n")
    out_bdcff.write("X=STEELOUTBOX\n")
    out_bdcff.write("H=HIDDENSTEELOUTBOX\n")
    out_bdcff.write("[/mapcodes]\n")
    out_bdcff.write("\n")
    out_bdcff.write("[game]\n")
    out_bdcff.write(f"Name={set_name}\n")
    out_bdcff.write(f"Author={auth_name}\n")
    # out_bdcff.write(f"Date={args["year"]}\n")
    # out_bdcff.write("Description=A converted Krissz Engine cave set\n")
    out_bdcff.write(f"KrisszEngine=true\n")
    out_bdcff.write(f"Lives=1\n")
    out_bdcff.write(f"BonusLife=0\n")
    out_bdcff.write("\n")

    filelist_sorted = get_sorted_file_list(src_dir, args["sort_files"])

    # convert all the caves
    for sortname in sorted(filelist_sorted.keys()):
        basename = filelist_sorted[sortname]
        filename = os.path.join(src_dir, basename)
        if basename.endswith(".html") or basename.endswith(".map"):
            f = open(filename, "r")
            data = f.read()
            f.close()
            if data.find(" _CR=") != -1:
                print(f"{args["prefix"]}Converting {basename} (new engine) ...")
                num_levels += 1
                args["year"] = convert_new_engine(filename, out_bdcff, num_levels, args)
            else:
                print(f"{args["prefix"]}Converting {basename} (old engine) ...")
                num_levels += 1
                convert_old_engine(filename, out_bdcff, num_levels, args)

    # write the levelinfo.conf file
    f = open(os.path.join(dest_dir, "levelinfo.conf"), "w")
    f.write(f"name:                           {set_name}\n")
    # f.write(f"name_sorting:                   {set_name}\n")
    f.write(f"author:                         {auth_name}\n")
    if args["year"] != "Unknown":
        f.write(f"year:                           {args["year"]}\n")
    f.write("\n")
    f.write(f"levels:                         {num_levels}\n")
    f.write("first_level:                    1\n")
    f.write("\n")
    f.write("bd_use_krissz_engine:           true\n")
    f.write("bd_initial_lives:               1\n")
    f.write("bd_maximum_lives:               1\n")
    f.write("bd_bonus_life_score:            0\n")
    f.write("\n")
    f.write("graphics_set:                   gfx_krissz_boulder_dash\n")
    f.write("sounds_set:                     snd_krissz_boulder_dash\n")
    f.write("music_set:                      mus_krissz_boulder_dash\n")
    f.close()

    # write the BDCFF footer
    out_bdcff.write("[/game]\n")
    out_bdcff.write("[/BDCFF]\n")
    out_bdcff.close()

def convert_single(src_dir, dest_dir, set_name, auth_name, args):
    if not os.path.exists(dest_dir):
        os.mkdir(dest_dir)

    # write the top level levelinfo.conf
    f = open(os.path.join(dest_dir, "levelinfo.conf"), "w")
    f.write(f"name:                         {set_name}\n")
    f.write(f"author:                       {auth_name}\n")
    f.write(f"latest_engine:                true\n")
    f.write(f"bd_use_krissz_engine:         true\n")
    f.write(f"level_group:                  true\n")
    f.write(f"readonly:                     false\n")
    f.close()

    filelist_sorted = get_sorted_file_list(src_dir, args["sort_files"])
    
    # convert all the caves
    for sortname in sorted(filelist_sorted.keys()):
        basename = filelist_sorted[sortname]
        filename = os.path.join(src_dir, basename)
        if basename.endswith(".html") or basename.endswith(".map"):
            f = open(filename, "r")
            data = f.read()
            f.close()

            caveName = ""
            creatorName = "Unknown"
            createTime = "Unknown"
            if data.find(" _CR=") != -1:
                _CR = []
                with open(filename, "r") as inp:
                    lines = inp.readlines()
                    for line in lines:
                        if line.find("_CR") != -1:
                            _CR_raw = line.split("_CR=[")[1].split("];")[0].split(",")
                            for code in _CR_raw:
                                _CR.append(int(code))
                            break
                cave_data = json.loads(decompress(_CR))
                caveName = cave_data["caveName"] if not args["capitalize"] else cave_data["caveName"].upper()
                creatorName = "Unknown" if "creatorName" not in cave_data else cave_data["creatorName"]
                createTime = "Unknown" if "createTime" not in cave_data else cave_data["createTime"].split(" ")[0]
            else:
                with open(filename, "r" ) as inp:
                    soup = BeautifulSoup(inp.read(), 'html.parser')
                    cave_data = soup.find('div', id='caveProperties')
                    caveName = cave_data["data-cavename"] if not args["capitalize"] else cave_data["data-cavename"].upper()

            # write the BDCFF header
            path_to_bd = os.path.join(dest_dir, caveName.replace("/", "-"))
            if not os.path.exists(path_to_bd):
                os.mkdir(path_to_bd)
            out_bdcff = open(os.path.join(path_to_bd, f"001.bd"), "w")
            out_bdcff.write("; converted using KrisszConvert by Michael Kamensky\n")
            out_bdcff.write("[BDCFF]\n")
            out_bdcff.write("Version=0.5\n")
            out_bdcff.write("\n")
            out_bdcff.write("[mapcodes]\n")
            out_bdcff.write("Length=1\n")
            out_bdcff.write("%=MEGABOULDER\n")
            out_bdcff.write("!=LIGHTBOULDER\n")
            out_bdcff.write("X=STEELOUTBOX\n")
            out_bdcff.write("H=HIDDENSTEELOUTBOX\n")
            out_bdcff.write("[/mapcodes]\n")
            out_bdcff.write("\n")
            out_bdcff.write("[game]\n")
            out_bdcff.write(f"Name={set_name}\n")
            out_bdcff.write(f"Author={auth_name if creatorName == "Unknown" else creatorName}\n")
            out_bdcff.write(f"Date={args["year"] if createTime == "Unknown" else createTime}\n")
            # out_bdcff.write("Description=A converted Krissz Engine cave\n")
            out_bdcff.write(f"KrisszEngine=true\n")
            out_bdcff.write(f"Lives=1\n")
            out_bdcff.write(f"BonusLife=0\n")
            out_bdcff.write("\n")

            if data.find(" _CR=") != -1:
                print(f"{args["prefix"]}Converting {basename} (new engine) ...")
                args["year"] = convert_new_engine(filename, out_bdcff, 1, args)
            else:
                print(f"{args["prefix"]}Converting {basename} (old engine) ...")
                convert_old_engine(filename, out_bdcff, 1, args)

            # write the levelinfo.conf file
            f = open(os.path.join(path_to_bd, "levelinfo.conf"), "w")
            f.write(f"name:                           {caveName}\n")
            # f.write(f"name_sorting:                   {caveName}\n")
            f.write(f"author:                         {auth_name if creatorName == "Unknown" else creatorName}\n")
            f.write(f"year:                           {args["year"] if createTime == "Unknown" else createTime}\n")
            f.write("\n")
            f.write("levels:                         1\n")
            f.write("first_level:                    1\n")
            f.write("\n")
            f.write("bd_use_krissz_engine:           true\n")
            f.write("bd_initial_lives:               1\n")
            f.write("bd_maximum_lives:               1\n")
            f.write("bd_bonus_life_score:            0\n")
            f.write("\n")
            f.write("graphics_set:                   gfx_krissz_boulder_dash\n")
            f.write("sounds_set:                     snd_krissz_boulder_dash\n")
            f.write("music_set:                      mus_krissz_boulder_dash\n")
            f.close()

            # write the BDCFF footer
            out_bdcff.write("[/game]\n")
            out_bdcff.write("[/BDCFF]\n")
            out_bdcff.close()


# -----------------------------------------------------------------------------
# main
# -----------------------------------------------------------------------------

def run():
    if len(sys.argv) < 5:
        print("Usage: krissz2rnd.py <source dir> <target dir> <level set name> <author name> [<year>] [ARGS ...]\n")
        print("    <source dir>     - directory with cave files (in map or html format)")
        print("    <target dir>     - directory wwhere to create the converted level set(s)")
        print("    <level set name> - the name of the (top) level set")
        print("    <author name>    - the name of the author of the level set(s)")
        print("    <year>           - the year of the level set(s) (optional)")
        print("")
        print("Additional arguments (optional):\n")
        print("    --capitalize     - capitalize cave names")
        print("    --dump-data      - dump cave data")
        print("    --sort-caves     - sort cave files according to numbers in file names")
        print("    --single-caves   - single cave mode (place each cave in its own subfolder)")
        print("    --rename-files   - rename cave files using normalized cave name and cave code")
        print("    --skip-old-demos - do not add existing replays for caves using old engine")
        print("    --use-prefix     - use predefined prefix to print informational output")
        print("    --messages <dir> - add level info messages to caves from directory <dir>")
        print("")
        exit(1)

    args = {}

    # make the folder if possible
    src_dir = sys.argv[1]
    dest_dir = sys.argv[2]
    set_name = sys.argv[3]
    auth_name = sys.argv[4]

    # add all mandatory arguments
    args["src_dir"] = src_dir
    args["dest_dir"] = dest_dir
    args["set_name"] = set_name
    args["auth_name"] = auth_name

    args["year"] = "Unknown"
    args["capitalize"] = False
    args["dump_data"] = False
    args["sort_files"] = False
    args["single_mode"] = False
    args["rename_files"] = False
    args["skip_old_demos"] = False
    args["prefix"] = ""
    args["message_dir"] = ""

    if len(sys.argv) > 5:
        for i in range(5, len(sys.argv)):
            if sys.argv[i] == '--capitalize' or sys.argv[i] == '-c':
                args["capitalize"] = True
            elif sys.argv[i] == '--dump-data' or sys.argv[i] == '-d':
                args["dump_data"] = True
            elif sys.argv[i] == '--sort-caves' or sys.argv[i] == '-r':
                args["sort_files"] = True
            elif sys.argv[i] == '--single-caves' or sys.argv[i] == '-s':
                args["single_mode"] = True
            elif sys.argv[i] == '--rename-files':
                args["rename_files"] = True
            elif sys.argv[i] == '--skip-old-demos':
                args["skip_old_demos"] = True
            elif sys.argv[i] == '--use-prefix':
                args["prefix"] = "            o "
            elif sys.argv[i] == '--messages' and i + 1 < len(sys.argv):
                args["message_dir"] = sys.argv[i + 1]
            elif i == 5:
                args["year"] = sys.argv[i]

    # check if source directory exists
    if not os.path.isdir(src_dir):
        print(f"ERROR: Directory '{src_dir}' not found!\n")
        exit(10)

    # check if (optional) message directory exists
    if args["message_dir"] != "" and not os.path.isdir(args["message_dir"]):
        print(f"ERROR: Directory '{args["message_dir"]}' not found!\n")
        exit(10)

    num_levels = 0

    # check if source directory contains any caves
    filelist = os.listdir(src_dir)
    filelist.sort()
    for basename in filelist:
        if basename.endswith(".html") or basename.endswith(".map"):
            num_levels += 1

    if num_levels == 0:
        print(f"ERROR: No caves found in directory '{src_dir}'!\n")
        exit(10)

    if args["single_mode"]:
        convert_single(src_dir, dest_dir, set_name, auth_name, args)
    else:
        convert_set(src_dir, dest_dir, set_name, auth_name, args)

if __name__ == "__main__":
    run()
