// Copyright (C) 2025 International Digital Economy Academy
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; version 2.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, see .

///|
async fn println_to_stderr(msg : String) -> Unit noraise {
  @stdio.stderr.write(msg + "\n") catch {
    _ => ()
  }
}

///|
#cfg(target="wasm")
fn runtime_exit(code : Int) -> Unit = "wasi_snapshot_preview1" "proc_exit"

///|
#cfg(target="native")
extern "c" fn runtime_exit(code : Int) -> Unit = "exit"

///|
fn[T] exit(code : Int) -> T {
  runtime_exit(code)
  panic()
}

///|
async fn write_text_file(file : String, content : String) -> Unit {
  @fs.write_file(file, content, create_mode=CreateOrTruncate)
}

///|
priv suberror CliError {
  UnsupportedMode(String)
  UnsupportedInputMode(String)
}

///|
priv struct CliOptions {
  input_file : String
  output_file : String
  output_map_file : String?
  mode : @driver.Mode
  input_mode : @driver.InputMode
  external_tokens : Bool
  no_comments : Bool
  no_std : Bool
  table : Bool
  compress : Bool
  token_payload_rewrite : @driver.TokenPayloadRewrite
  force_int_position : Bool
  print_as_mly_without_actions : Bool
}

///|
fn cli_command() -> @argparse.Command {
  Command(
    "moonyacc",
    version="moonyacc 0.1.0",
    disable_version_flag=true,
    flags=[
      FlagArg(
        "external-tokens",
        long="external-tokens",
        about="Use external tokens",
      ),
      FlagArg(
        "no-comments",
        long="no-comments",
        about="Do not include comments in the output",
      ),
      FlagArg("no-std", long="no-std", about="Do not include standard library"),
      FlagArg("table", long="table", about="Use table engine"),
      FlagArg(
        "compress-table",
        long="compress-table",
        about="Use table engine with compression",
      ),
      FlagArg(
        "version",
        short='v',
        long="version",
        action=Version,
        about="Show version",
      ),
      FlagArg(
        "print-as-mly-without-actions",
        long="print-as-mly-without-actions",
        about="Print as mly without actions",
      ),
      FlagArg(
        "force-token-json-payload",
        long="force-token-json-payload",
        about="Force token to use JSON payload",
      ),
      FlagArg(
        "force-token-no-payload",
        long="force-token-no-payload",
        about="Force token to not use payload",
      ),
      FlagArg(
        "force-int-position",
        long="force-int-position",
        about="Overwrite the position type to int",
      ),
    ],
    options=[
      OptionArg(
        "output-file",
        short='o',
        long="output-file",
        about="Output file",
      ),
      OptionArg(
        "output-map-file",
        long="output-map-file",
        about="Output source map file, if not specified, it will be the output file with .map.json extension",
      ),
      OptionArg(
        "mode",
        long="mode",
        about="Specify mode (default, json-cst, only-tokens)",
      ),
      OptionArg(
        "input-mode",
        long="input-mode",
        about="Specify input mode (array, pull). Default is array",
      ),
    ],
    positionals=[
      PositionArg("input-file", num_args=@argparse.ValueRange::single()),
    ],
  )
}

///|
fn string_value(
  matches : @argparse.Matches,
  name : String,
  default : String,
) -> String {
  match matches.values.get(name) {
    Some(values) => values[0]
    None => default
  }
}

///|
fn string_option(matches : @argparse.Matches, name : String) -> String? {
  match matches.values.get(name) {
    Some(values) => Some(values[0])
    None => None
  }
}

///|
fn flag_value(matches : @argparse.Matches, name : String) -> Bool {
  matches.flags.get(name).unwrap_or(false)
}

///|
fn parse_mode(mode_sym : String) -> @driver.Mode raise CliError {
  match mode_sym {
    "default" => Default
    "json-cst" => JsonCst
    "only-tokens" => OnlyTokens
    _ => raise UnsupportedMode(mode_sym)
  }
}

///|
fn parse_input_mode(
  input_mode_sym : String,
) -> @driver.InputMode raise CliError {
  match input_mode_sym {
    "array" => Array
    "pull" => Pull
    _ => raise UnsupportedInputMode(input_mode_sym)
  }
}

///|
fn parse_token_payload_rewrite(
  argv : ArrayView[String],
) -> @driver.TokenPayloadRewrite {
  let mut rewrite : @driver.TokenPayloadRewrite = NoRewrite
  for arg in argv {
    match arg {
      "--force-token-json-payload" => rewrite = JsonPayload
      "--force-token-no-payload" => rewrite = NoPayload
      _ => ()
    }
  }
  rewrite
}

///|
fn parse_cli_args(argv : ArrayView[String]) -> CliOptions raise {
  let matches = cli_command().parse(argv~, env={})
  let compress = flag_value(matches, "compress-table")
  {
    input_file: string_value(matches, "input-file", ""),
    output_file: string_value(matches, "output-file", ""),
    output_map_file: string_option(matches, "output-map-file"),
    mode: parse_mode(string_value(matches, "mode", "default")),
    input_mode: parse_input_mode(string_value(matches, "input-mode", "array")),
    external_tokens: flag_value(matches, "external-tokens"),
    no_comments: flag_value(matches, "no-comments"),
    no_std: flag_value(matches, "no-std"),
    table: compress || flag_value(matches, "table"),
    compress,
    token_payload_rewrite: parse_token_payload_rewrite(argv),
    force_int_position: flag_value(matches, "force-int-position"),
    print_as_mly_without_actions: flag_value(
      matches, "print-as-mly-without-actions",
    ),
  }
}

///|
async fn main {
  let argv = @env.args()
  let options = parse_cli_args(if argv.length() > 1 { argv[1:] } else { [] }) catch {
    CliError::UnsupportedMode(mode_sym) => {
      println_to_stderr("Unsupported mode: \{mode_sym}")
      exit(1)
    }
    CliError::UnsupportedInputMode(input_mode_sym) => {
      println_to_stderr("Unsupported input mode: \{input_mode_sym}")
      exit(1)
    }
    err => {
      println_to_stderr(err.to_string())
      exit(1)
    }
  }
  let parser_spec_src = @fs.read_file(options.input_file).text() catch {
    err => abort(Show::to_string(err))
  }
  if options.print_as_mly_without_actions {
    let out = StringBuilder::new()
    @driver.print(parser_spec_src, filename=options.input_file, out~)
    exit(0)
  }
  let source_map = @codegen.SourceMap::new()
  let output = @driver.compile(
    parser_spec_src,
    mode=options.mode,
    source_map_builder=source_map,
    input_mode=options.input_mode,
    filename=options.input_file,
    external_tokens=options.external_tokens,
    no_comments=options.no_comments,
    no_std=options.no_std,
    token_payload_rewrite=options.token_payload_rewrite,
    force_int_position=options.force_int_position,
    generator=if options.table {
      if options.compress {
        @gen_mbt_table.compress_generator
      } else {
        @gen_mbt_table.generator
      }
    } else {
      @gen_mbt.generator
    },
  )

  if options.output_file == "" {
    @stdio.stdout.write(output + "\n")
    match options.output_map_file {
      Some(file) =>
        try! write_text_file(file, source_map.to_json().stringify(indent=2))
      None => ()
    }
  } else {
    try! ({
      write_text_file(options.output_file, output)
      write_text_file(
        options.output_map_file.unwrap_or("\{options.output_file}.map.json"),
        source_map.to_json().stringify(indent=2),
      )
    })
  }
}