#!/usr/bin/env python3

# --------------------------------------------------------------------
# SPDX-License-Identifier: AGPL-3.0-or-later
# © Copyright 2008-2024 José Manuel Rodríguez de la Rosa and contributors.
# See the file CONTRIBUTORS.md for copyright details.
# See https://www.gnu.org/licenses/agpl-3.0.html for details.
# --------------------------------------------------------------------

import argparse
import os
import sys
from enum import StrEnum

import src.api.config
from src.api import errmsg, global_
from src.api.config import OPTIONS
from src.api.errmsg import warning_command_line_flag_deprecation
from src.zxbasm import asmparse, expr
from src.zxbasm.version import VERSION
from src.zxbpp import zxbpp
from src.zxbpp.zxbpp import PreprocMode


class FileType(StrEnum):
    BIN = "bin"
    SNA = "sna"
    TAP = "tap"
    TZX = "tzx"
    Z80 = "z80"


def main(args=None):
    # Initializes asm parser state
    src.api.config.init()
    asmparse.init()
    zxbpp.init()

    # Create option parser
    o_parser = argparse.ArgumentParser()
    o_parser.add_argument("PROGRAM", type=str, help="ASM program file")
    o_parser.add_argument(
        "-d", "--debug", action="count", default=OPTIONS.debug_level, help="Enable verbosity/debugging output"
    )

    o_parser.add_argument(
        "-O",
        "--optimize",
        type=int,
        dest="optimization_level",
        help="Sets optimization level. 0 = None",
        default=OPTIONS.optimization_level,
    )

    o_parser.add_argument(
        "-o",
        "--output",
        type=str,
        dest="output_file",
        help="Sets output file. Default is input filename with .bin extension",
        default=None,
    )

    output_file_type_group = o_parser.add_mutually_exclusive_group()
    output_file_type_group.add_argument(
        "-T",
        "--tzx",
        action="store_true",
        dest="tzx",
        default=False,
        help="Sets output format to tzx (default is .bin)",
    )

    output_file_type_group.add_argument(
        "-t",
        "--tap",
        action="store_true",
        dest="tap",
        default=False,
        help="Sets output format to tzx (default is .bin)",
    )

    output_file_type_group.add_argument(
        "-f",
        "--output-format",
        type=str,
        choices=[str(x) for x in FileType],
        required=False,
        help="Output format",
        default=OPTIONS.output_file_type,
    )

    o_parser.add_argument(
        "-B",
        "--BASIC",
        action="store_true",
        dest="basic",
        default=False,
        help="Creates a BASIC loader which load the rest of the CODE. Requires -T ot -t",
    )

    o_parser.add_argument(
        "-a",
        "--autorun",
        action="store_true",
        default=False,
        help="Sets the program to auto run once loaded (implies --BASIC)",
    )

    o_parser.add_argument(
        "-e",
        "--errmsg",
        type=str,
        dest="stderr",
        default=OPTIONS.stderr_filename,
        help="Error messages file (standard error console by default",
    )

    o_parser.add_argument("-M", "--mmap", type=str, dest="memory_map", default=None, help="Generate label memory map")

    o_parser.add_argument(
        "-b",
        "--bracket",
        action="store_true",
        default=False,
        help="Allows brackets only for memory access and indirections",
    )

    o_parser.add_argument(
        "-N", "--zxnext", action="store_true", default=False, help="Enable ZX Next extra ASM opcodes!"
    )

    o_parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)

    options = o_parser.parse_args(args)

    if not os.path.exists(options.PROGRAM):
        o_parser.error("No such file or directory: '%s'" % options.PROGRAM)

    OPTIONS.debug_level = int(options.debug)
    OPTIONS.input_filename = options.PROGRAM
    OPTIONS.output_filename = options.output_file
    OPTIONS.optimization_level = options.optimization_level
    OPTIONS.use_basic_loader = options.autorun or options.basic
    OPTIONS.autorun = options.autorun
    OPTIONS.stderr_filename = options.stderr
    OPTIONS.memory_map = options.memory_map
    OPTIONS.force_asm_brackets = options.bracket
    OPTIONS.zxnext = options.zxnext

    if options.output_format:
        OPTIONS.output_file_type = options.output_format
    elif options.tzx:
        OPTIONS.output_file_type = FileType.TZX
        warning_command_line_flag_deprecation(
            f"--tzx (use -f {FileType.TZX} or --output-format={FileType.TZX} instead)"
        )
    elif options.tap:
        OPTIONS.output_file_type = FileType.TAP
        warning_command_line_flag_deprecation(
            f"--tap (use -f {FileType.TAP} or --output-format={FileType.TAP} instead)"
        )

    if not OPTIONS.output_filename:
        OPTIONS.output_filename = (
            os.path.splitext(os.path.basename(OPTIONS.input_filename))[0] + os.path.extsep + OPTIONS.output_file_type
        )

    if OPTIONS.stderr_filename:
        OPTIONS.stderr = open(OPTIONS.stderr_filename, "wt")

    if int(options.tzx) + int(options.tap) > 1:
        o_parser.error("Options --tap, --tzx and --asm are mutually exclusive")
        return 3

    if OPTIONS.use_basic_loader and not options.tzx and not options.tap:
        o_parser.error("Option --BASIC and --autorun requires --tzx or tap format")
        return 4

    # Configure the preprocessor to use the asm-preprocessor-lexer
    zxbpp.setMode(PreprocMode.ASM)

    # Now filter them against the preprocessor
    zxbpp.main([OPTIONS.input_filename])

    # Now output the result
    asm_output = zxbpp.OUTPUT
    asmparse.assemble(asm_output)
    if global_.has_errors:
        return 1

    if not asmparse.MEMORY.memory_bytes:  # empty seq.
        errmsg.warning(0, "Nothing to assemble. Exiting...")
        return 0

    current_org = max(asmparse.MEMORY.memory_bytes.keys() or [0]) + 1

    for label, line in asmparse.INITS:
        expr_label = expr.Expr.makenode(asmparse.Container(asmparse.MEMORY.get_label(label, line), line))
        asmparse.MEMORY.add_instruction(asmparse.Asm(0, "CALL NN", expr_label))

    if len(asmparse.INITS) > 0:
        if asmparse.AUTORUN_ADDR is not None:
            asmparse.MEMORY.add_instruction(asmparse.Asm(0, "JP NN", asmparse.AUTORUN_ADDR))
        else:
            asmparse.MEMORY.add_instruction(
                asmparse.Asm(0, "JP NN", min(asmparse.MEMORY.orgs.keys()))
            )  # To the beginning of binary

        asmparse.AUTORUN_ADDR = current_org

    if OPTIONS.memory_map:
        with open(OPTIONS.memory_map, "wt") as f:
            f.write(asmparse.MEMORY.memory_map)

    asmparse.generate_binary(OPTIONS.output_filename, OPTIONS.output_file_type)
    return global_.has_errors


if __name__ == "__main__":
    sys.exit(main())
