///|
/// Stable JSON reporting used by both single-file and directory scans.
///
/// Address-like values are strings in hexadecimal form so that JavaScript
/// consumers do not lose precision above 2^53.

///|
fn json_number(value : Int32) -> Json {
  Json::number(value.to_double())
}

///|
fn json_number64(value : Int64) -> Json {
  Json::number(value.to_double())
}

///|
fn json_hex(value : Int32) -> Json {
  Json::string("0x" + @pe.hex64(value.to_int64() & 0xFFFFFFFFL))
}

///|
fn json_hex64(value : Int64) -> Json {
  Json::string("0x" + @pe.hex64(value))
}

///|
fn json_error(error : @pe.PeError) -> Json {
  Json::object({ "error": Json::string(error.message()) })
}

///|
fn json_optional_string(value : String?) -> Json {
  match value {
    Some(text) => Json::string(text)
    None => Json::null()
  }
}

///|
fn json_optional_number(value : Int32?) -> Json {
  match value {
    Some(number) => json_number(number)
    None => Json::null()
  }
}

///|
fn json_import_entries(entries : Array[@pe.ImportEntry]) -> Json {
  Json::array(
    entries.map(fn(entry) {
      {
        "name": json_optional_string(entry.name),
        "ordinal": json_optional_number(entry.ordinal),
      }
    }),
  )
}

///|
fn json_info(path : String, pe : @pe.Pe) -> Json {
  let export_count = try {
    match pe.exports() {
      Some(table) => table.exports.length()
      None => 0
    }
  } catch {
    _ => 0
  }
  let iat_count = pe.iat().length() catch { _ => 0 }
  Json::object({
    "file": Json::string(path),
    "format": json_number(pe.bits()),
    "machine": Json::string(pe.machine()),
    "subsystem": Json::string(pe.subsystem()),
    "image_base": json_hex64(pe.optional.image_base),
    "image_size": json_hex(pe.optional.size_of_image),
    "entry_point": json_hex(pe.optional.entry_point),
    "checksum": json_hex(pe.optional.checksum),
    "sections": json_number(pe.sections.length()),
    "import_libraries": json_number(pe.imports().length() catch { _ => 0 }),
    "delay_import_libraries": json_number(
      pe.delay_imports().length() catch {
        _ => 0
      },
    ),
    "iat_entries": json_number(iat_count),
    "exports": json_number(export_count),
  })
}

///|
fn json_sections(pe : @pe.Pe) -> Json {
  Json::array(
    pe.sections.map(fn(section) {
      {
        "name": Json::string(section.name),
        "virtual_address": json_hex(section.virtual_address),
        "virtual_size": json_hex(section.virtual_size),
        "raw_pointer": json_hex(section.raw_pointer),
        "raw_size": json_hex(section.raw_size),
        "characteristics": json_hex(section.characteristics),
        "permissions": Json::string(section.permissions()),
      }
    }),
  )
}

///|
fn json_imports(pe : @pe.Pe) -> Json raise @pe.PeError {
  let normal : Array[Json] = []
  for library in pe.imports() {
    normal.push({
      "library": Json::string(library.name),
      "entries": json_import_entries(library.entries),
    })
  }
  let delayed : Array[Json] = []
  for library in pe.delay_imports() {
    delayed.push({
      "library": Json::string(library.name),
      "rva_based": Json::boolean(library.rva_based),
      "iat_rva": match library.iat_rva {
        Some(rva) => json_hex(rva)
        None => Json::null()
      },
      "entries": json_import_entries(library.entries),
    })
  }
  { "static": Json::array(normal), "delay_loaded": Json::array(delayed) }
}

///|
fn json_iat(pe : @pe.Pe) -> Json raise @pe.PeError {
  Json::array(
    pe
    .iat()
    .map(fn(entry) {
      {
        "library": Json::string(entry.library),
        "delayed": Json::boolean(entry.delayed),
        "slot_rva": json_hex(entry.slot_rva),
        "raw_value": json_hex64(entry.raw_value),
        "symbol": {
          "name": json_optional_string(entry.symbol.name),
          "ordinal": json_optional_number(entry.symbol.ordinal),
        },
      }
    }),
  )
}

///|
fn json_exports(pe : @pe.Pe) -> Json raise @pe.PeError {
  match pe.exports() {
    None => Json::null()
    Some(table) =>
      {
        "dll_name": Json::string(table.dll_name),
        "entries": Json::array(
          table.exports.map(fn(entry) {
            {
              "name": json_optional_string(entry.name),
              "ordinal": json_number(entry.ordinal),
              "rva": json_hex(entry.rva),
            }
          }),
        ),
      }
  }
}

///|
fn json_debug(pe : @pe.Pe) -> Json raise @pe.PeError {
  Json::array(
    pe
    .debug_entries()
    .map(fn(entry) {
      {
        "type": json_number(entry.kind),
        "type_name": Json::string(entry.kind_name),
        "timestamp": json_number(entry.timestamp),
        "size": json_number(entry.size),
        "address": json_hex(entry.address),
        "pdb_signature": json_optional_string(entry.pdb_signature),
        "pdb_age": json_optional_number(entry.pdb_age),
        "pdb_path": json_optional_string(entry.pdb_path),
      }
    }),
  )
}

///|
fn json_relocations(pe : @pe.Pe) -> Json raise @pe.PeError {
  let summary = pe.relocations()
  {
    "blocks": Json::array(
      summary.blocks.map(fn(block) {
        {
          "page_rva": json_hex(block.page_rva),
          "entries": json_number(block.entries),
          "dir64": json_number(block.dir64),
          "highlow": json_number(block.highlow),
          "absolute": json_number(block.absolute),
          "other": json_number(block.other),
          "high": json_number(block.high),
          "low": json_number(block.low),
        }
      }),
    ),
    "total_entries": json_number(summary.total_entries),
    "dir64": json_number(summary.dir64),
    "highlow": json_number(summary.highlow),
  }
}

///|
fn json_resource_content(content : @pe.ResourceContent) -> Json {
  {
    "kind": Json::string(content.kind),
    "summary": Json::string(content.summary),
    "strings": Json::array(
      content.strings.map(fn(item) {
        { "id": json_number(item.id), "text": Json::string(item.text) }
      }),
    ),
    "group_icons": Json::array(
      content.group_icons.map(fn(item) {
        {
          "width": json_number(item.width),
          "height": json_number(item.height),
          "color_count": json_number(item.color_count),
          "planes": json_number(item.planes),
          "bit_count": json_number(item.bit_count),
          "bytes": json_number(item.bytes),
          "resource_id": json_number(item.resource_id),
        }
      }),
    ),
    "width": json_optional_number(content.width),
    "height": json_optional_number(content.height),
    "bit_count": json_optional_number(content.bit_count),
    "image_bytes": json_optional_number(content.image_bytes),
    "png": Json::boolean(content.png),
    "text": json_optional_string(content.text),
    "version": match content.version {
      Some(info) =>
        Json::object({
          "file_version": Json::string(info.file_version),
          "product_version": Json::string(info.product_version),
          "company_name": json_optional_string(info.company_name),
          "product_name": json_optional_string(info.product_name),
          "file_description": json_optional_string(info.file_description),
          "original_filename": json_optional_string(info.original_filename),
        })
      None => Json::null()
    },
  }
}

///|
fn json_resources(pe : @pe.Pe) -> Json raise @pe.PeError {
  Json::array(
    pe
    .resources()
    .map(fn(entry) {
      let decoded = json_resource_content(pe.decode_resource(entry)) catch {
        e => json_error(e)
      }
      {
        "type_id": json_optional_number(entry.type_id),
        "type_name": json_optional_string(entry.type_name),
        "name_id": json_optional_number(entry.name_id),
        "name_name": json_optional_string(entry.name_name),
        "language": json_hex(entry.language),
        "rva": json_hex(entry.rva),
        "size": json_number(entry.size),
        "code_page": json_number(entry.code_page),
        "decoded": decoded,
      }
    }),
  )
}

///|
fn json_load_config(pe : @pe.Pe) -> Json raise @pe.PeError {
  match pe.load_config() {
    None => Json::null()
    Some(info) => {
      let handlers = pe.safe_seh_handlers()
      let safe_seh = match handlers {
        None => Json::string("not-applicable")
        Some(list) => Json::array(list.map(fn(rva) { json_hex(rva) }))
      }
      {
        "size": json_hex(info.size),
        "security_cookie": json_hex64(info.security_cookie),
        "safe_seh": safe_seh,
        "seh_handler_count": json_number64(info.seh_handler_count),
        "guard_flags": json_hex(info.guard_flags),
        "cfg_table": json_hex64(info.guard_cf_function_table),
        "cfg_count": json_number64(info.guard_cf_function_count),
        "heap_flags": json_hex(info.process_heap_flags),
      }
    }
  }
}

///|
fn json_check(pe : @pe.Pe) -> Json {
  let report = pe.check()
  {
    "verdict": Json::string(report.verdict),
    "max_section_entropy": Json::number(report.max_section_entropy),
    "findings": Json::array(
      report.findings.map(fn(finding) {
        let severity = match finding.severity {
          @pe.Info => "info"
          @pe.Warning => "warning"
          @pe.High => "high"
        }
        {
          "severity": Json::string(severity),
          "code": Json::string(finding.code),
          "message": Json::string(finding.message),
        }
      }),
    ),
  }
}

///|
fn json_dotnet(pe : @pe.Pe) -> Json raise @pe.PeError {
  match pe.clr_metadata() {
    None => Json::null()
    Some(info) =>
      {
        "module": json_optional_string(info.module_name),
        "assembly": json_optional_string(info.assembly_name),
        "version": json_optional_string(info.assembly_version),
        "flags": json_hex(info.assembly_flags),
        "streams": Json::array(
          info.streams.map(fn(stream) {
            {
              "name": Json::string(stream.name),
              "offset": json_hex(stream.offset),
              "size": json_number(stream.size),
            }
          }),
        ),
        "tables": Json::array(
          info.tables.map(fn(table) {
            {
              "name": Json::string(table.name),
              "rows": json_number(table.rows),
            }
          }),
        ),
        "assembly_references": Json::array(
          info.assembly_references.map(fn(reference) {
            {
              "name": Json::string(reference.name),
              "version": Json::string(reference.version),
              "flags": json_hex(reference.flags),
              "public_key_or_token": json_hex(reference.public_key_or_token),
            }
          }),
        ),
        "types": Json::array(
          info.type_definitions.map(fn(item) {
            {
              "namespace": Json::string(item.namespace_name),
              "name": Json::string(item.name),
              "flags": json_hex(item.flags),
            }
          }),
        ),
        "type_references": Json::array(
          info.type_references.map(fn(item) {
            {
              "namespace": Json::string(item.namespace_name),
              "name": Json::string(item.name),
              "flags": json_hex(item.flags),
            }
          }),
        ),
        "methods": Json::array(
          info.method_definitions.map(fn(method_info) {
            {
              "name": Json::string(method_info.name),
              "rva": json_hex(method_info.rva),
              "flags": json_hex(method_info.flags),
              "impl_flags": json_hex(method_info.impl_flags),
            }
          }),
        ),
        "member_references": Json::array(
          info.member_reference_names.map(fn(name) { Json::string(name) }),
        ),
      }
  }
}

///|
fn json_for_command(path : String, command : String, pe : @pe.Pe) -> Json {
  let (data, ok) = try {
    let value = if command == "info" {
      json_info(path, pe)
    } else if command == "sections" {
      json_sections(pe)
    } else if command == "imports" {
      json_imports(pe)
    } else if command == "iat" {
      json_iat(pe)
    } else if command == "exports" {
      json_exports(pe)
    } else if command == "debug" {
      json_debug(pe)
    } else if command == "relocations" {
      json_relocations(pe)
    } else if command == "resources" {
      json_resources(pe)
    } else if command == "loadconfig" {
      json_load_config(pe)
    } else if command == "dotnet" {
      json_dotnet(pe)
    } else if command == "check" {
      json_check(pe)
    } else {
      Json::object({
        "info": json_info(path, pe),
        "sections": json_sections(pe),
        "imports": json_imports(pe),
        "iat": json_iat(pe),
        "exports": json_exports(pe),
        "debug": json_debug(pe),
        "relocations": json_relocations(pe),
        "resources": json_resources(pe),
        "loadconfig": json_load_config(pe),
        "dotnet": json_dotnet(pe),
        "check": json_check(pe),
      })
    }
    (value, true)
  } catch {
    e => (json_error(e), false)
  }
  {
    "schema": Json::string("moonpe/1"),
    "command": Json::string(command),
    "file": Json::string(path),
    "ok": Json::boolean(ok),
    "data": data,
  }
}

///|
fn print_json(value : Json) -> Unit {
  println(value.stringify())
}

///|
fn fail_json(message : String) -> Unit {
  print_json({
    "schema": Json::string("moonpe/1"),
    "ok": Json::boolean(false),
    "error": Json::string(message),
  })
}