///|
async fn main {
  let files = []
  @ArgParser.parse([], path => files.push(path), "", @env.args()[1:].to_array())
  for file in files {
    if !@fs.exists(file) {
      abort("file not found: \{file}")
    }
    let result = @fs.read_file(file).text() |> process_file(name=file)
    @fs.write_file(file, result, truncate=true)
    let _ = @process.collect_output("moonfmt", ["-w", file])
    println("written to \{file}")
  }
}

///|
let jmop_pkg = "@Yoorkin/jmop"

///|
let jmop_optional = "\{jmop_pkg}.Optional"

///|
let jmop_object = "\{jmop_pkg}.Object"

///|
let jmop_nullable = "\{jmop_pkg}.Nullable"

///|
let jmop_convert_error = "\{jmop_pkg}.convert_error"

///|
fn process_file(content : String, name~ : String) -> String {
  let content = remove_block_line(content)
  let (impls, reports) = @parser.parse_string(content)
  if !reports.is_empty() {
    println(reports)
    abort("failed to parse \{name}")
  }
  let directives = impls.map(extract_directive)
  let patches = generate_patch(directives)
  apply_patch0(content, patches)
}

///|
enum TypeAnnotation {
  Constr(@syntax.LongIdent, Array[TypeAnnotation])
  Option(TypeAnnotation)
  Tuple(Array[TypeAnnotation])
  Arrow(Array[TypeAnnotation], TypeAnnotation, err~ : ErrorTypeAnnoatation)
  Object(@syntax.LongIdent)
} derive(ToJson)

///|
pub(all) enum ErrorTypeAnnoatation {
  ErrorType(TypeAnnotation)
  DefaultErrorType
  NoErrorType
  Noraise
  MaybeError(TypeAnnotation)
} derive(ToJson)

///|
enum ParamDirective {
  Positional(name~ : String, ty~ : TypeAnnotation)
  Optional(name~ : String, ty~ : TypeAnnotation)
  Labeled(name~ : String, ty~ : TypeAnnotation)
} derive(ToJson)

///|
pub fn ParamDirective::name(self : Self) -> String {
  match self {
    Positional(name~, ..) | Optional(name~, ..) | Labeled(name~, ..) => name
  }
}

///|
fn process_error_type(ety : @syntax.ErrorType) -> ErrorTypeAnnoatation {
  match ety {
    ErrorType(ty~) => ErrorType(process_type_annotation(ty))
    DefaultErrorType(..) => DefaultErrorType
    NoErrorType => NoErrorType
    Noraise(..) => Noraise
    MaybeError(ty~) => MaybeError(process_type_annotation(ty))
  }
}

///|
fn process_type_annotation(ty : @syntax.Type) -> TypeAnnotation {
  match ty {
    Option(ty~, ..) => Option(process_type_annotation(ty))
    Name(constr_id~, tys~, ..) =>
      Constr(constr_id.id, tys.map(process_type_annotation).to_array())
    Tuple(tys~, ..) => Tuple(tys.map(process_type_annotation).to_array())
    Arrow(args~, res~, err~, ..) =>
      Arrow(
        args.map(process_type_annotation).to_array(),
        process_type_annotation(res),
        err=process_error_type(err),
      )
    Object(constr_id) => Object(constr_id.id)
    Any(_) => panic() // TODO: report error
  }
}

///|
fn process_parameter(param : @syntax.Parameter) -> ParamDirective {
  match param {
    DiscardPositional(ty~, loc~) => {
      ignore((ty, loc))
      abort("discard parameter is not supported")
    }
    Positional(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Positional(name=binder.name, ty=process_type_annotation(ty))
    }
    Labelled(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Labeled(name=binder.name, ty=process_type_annotation(ty))
    }
    Optional(binder~, default~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      ignore(default) //TODO: use default value
      Optional(name=binder.name, ty=process_type_annotation(ty))
    }
    QuestionOptional(binder~, ty~) => {
      guard ty is Some(ty)  // TODO: handle error case
      Optional(name=binder.name, ty=process_type_annotation(ty))
    }
  }
}

///|
enum Directive {
  Type(name~ : String, hole~ : Location)
  Constructor(
    ty~ : String,
    arguments~ : Map[String, ParamDirective],
    throws~ : Array[String],
    hole~ : Location,
    source_loc~ : String?
  )
  Getter(
    name~ : String,
    ty~ : String,
    result~ : TypeAnnotation,
    throws~ : Array[String],
    hole~ : Location,
    source_loc~ : String?
  )
  Setter(
    name~ : String,
    ty~ : String,
    value~ : ParamDirective?,
    throws~ : Array[String],
    hole~ : Location,
    source_loc~ : String?
  )
  Function(
    name~ : String,
    ty~ : String,
    arguments~ : Map[String, ParamDirective],
    result~ : TypeAnnotation,
    throws~ : Array[String],
    hole~ : Location,
    source_loc~ : String?
  )
  Method(
    name~ : String,
    ty~ : String,
    arguments~ : Map[String, ParamDirective],
    result~ : TypeAnnotation,
    throws~ : Array[String],
    hole~ : Location,
    source_loc~ : String?
  )
  Generated(hole~ : Location)
} derive(ToJson)

///|
fn extract_directive(toplevel : @syntax.Impl) -> Directive {
  let attrs = match toplevel {
    TopTypeDef(t) => t.attrs
    TopFuncDef(fun_decl~, ..) => fun_decl.attrs
    _ => @list.empty()
  }
  let exprs = attrs.filter_map(attr => match attr.parsed {
    Some(Apply({ qual: Some("webgen"), name }, props)) =>
      Some((name, props, attr.loc))
    Some(Ident({ qual: Some("webgen"), name })) =>
      Some((name, @list.empty(), attr.loc))
    _ => None
  })
  guard exprs is More(main, tail=rest) else {
    abort("expected ty, constructor, getter, setter")
  }
  if main.0 == "generated" {
    return Generated(hole=toplevel.loc())
  }
  let throws = []
  let arguments = {}
  let (name, ty, hole, result) = match toplevel {
    TopFuncDef(
      fun_decl={
        name: { name, .. },
        type_name: Some({ name: Ident(name=ty), .. }),
        decl_params,
        return_type,
        ..,
      },
      decl_body=DeclBody(expr~, ..),
      ..
    ) => {
      let name = if main.0 is ("getter" | "setter") {
        let trimed = if name.has_prefix("get_") || name.has_prefix("set_") {
          try! name[4:]
        } else if name.has_prefix("is_") {
          try! name[3:]
        } else {
          name
        }
        snake_to_small_camel(trimed)
      } else {
        name
      }
      for param in decl_params.unwrap() {
        let d = process_parameter(param)
        arguments[d.name()] = d
      }
      let result = return_type.map(process_type_annotation)
      (name, ty, expr.loc(), result)
    }
    TopTypeDef({ tycon, loc, .. }) => (tycon, tycon, loc, None)
    _ => abort("\{toplevel.loc()}: missing type name or binder name")
  }
  fn filter_name_prop(props : List[@attribute.Prop]) {
    let result = props.filter_map(fn(prop) {
      match prop {
        Labeled("name", String(v)) => Some(v)
        _ => None
      }
    })
    match result {
      More(r, tail=Empty) => Some(r)
      Empty => None
      _ => panic() // report error
    }
  }

  let name = if filter_name_prop(main.1) is Some(v) { v } else { name }
  for expr in rest {
    let (name, props, loc) = expr
    match name {
      "throws" =>
        for prop in props {
          match prop {
            Expr(String(s)) => throws.push(s)
            _ => abort("\{loc}: invalid throws argument")
          }
        }
      _ => abort("\{loc}: unknown webgen attribute: \{name}")
    }
  }
  // TODO: better error handling
  let source_loc = if arguments.contains("loc") { Some("loc") } else { None }
  match main.0 {
    "ty" => Type(name~, hole~)
    "constructor" => Constructor(arguments~, ty~, throws~, hole~, source_loc~)
    "getter" => {
      guard result is Some(result) else {
        abort("getter must have return type")
      }
      Getter(name~, ty~, result~, throws~, hole~, source_loc~)
    }
    "setter" => {
      let value = arguments.get("value")
      Setter(name~, ty~, value~, throws~, hole~, source_loc~)
    }
    "function" => {
      guard result is Some(result) else {
        abort("function must have return type")
      }
      Function(name~, ty~, arguments~, result~, throws~, hole~, source_loc~)
    }
    "method" => {
      guard result is Some(result) else {
        abort("function must have return type")
      }
      Method(name~, ty~, arguments~, result~, throws~, hole~, source_loc~)
    }
    _ => abort("\{hole}, unknown webgen attribute: \{main.0}")
  }
}

///|
enum Patch {
  Replace(Location, String)
  Append(String)
  Remove(Location)
} derive(Show, ToJson)

///|
fn longident_to_string(id : @syntax.LongIdent) -> String {
  match id {
    Dot(pkg~, id~) => "\{pkg}.\{id}"
    Ident(name~) => name
  }
}

///|
fn generate_ffi_type_annotation(ty : TypeAnnotation) -> String {
  match ty {
    Arrow(args, res, err~) => {
      ignore((args, res, err))
      abort("function type is not supported")
    }
    Tuple(tys) => "(" + tys.map(generate_ffi_type_annotation).join(", ") + ")"
    Object(name) => "&" + longident_to_string(name)
    Option(ty) => "Nullable[\{generate_ffi_type_annotation(ty)}]"
    Constr(name, args) => {
      let name = longident_to_string(name)
      if args.is_empty() {
        name
      } else {
        name + "[" + args.map(generate_ffi_type_annotation).join(", ") + "]"
      }
    }
  }
}

///|
fn generate_ffi_params(arguments : Map[String, ParamDirective]) -> String {
  let buf = StringBuilder::new()
  let mut first = true
  for _, arg in arguments {
    if first {
      first = false
    } else {
      buf.write_string(", ")
    }
    match arg {
      Positional(name~, ty~) =>
        buf.write_string("\{name} : \{generate_ffi_type_annotation(ty)}")
      Optional(name~, ty~) =>
        buf.write_string(
          "\{name} : \{jmop_optional}[\{generate_ffi_type_annotation(ty)}]",
        )
      Labeled(name~, ty~) =>
        buf.write_string("\{name} : \{generate_ffi_type_annotation(ty)}")
    }
  }
  buf.to_string()
}

///|
fn generate_glude_args(arguments : Map[String, ParamDirective]) -> String {
  let buf = StringBuilder::new()
  let mut first = true
  fn handle_nullable(arg_name : String, ty : TypeAnnotation) -> String {
    match ty {
      Option(_) => "\{jmop_optional}::from_option(\{arg_name})"
      _ => arg_name
    }
  }

  for _, arg in arguments {
    if first {
      first = false
    } else {
      buf.write_string(", ")
    }
    match arg {
      Optional(name~, ty~) =>
        buf
        ..write_string("\{jmop_optional}::from_option(")
        ..write_string(handle_nullable(name, ty))
        ..write_string(")")
      Positional(name~, ty~) | Labeled(name~, ty~) =>
        buf.write_string(handle_nullable(name, ty))
    }
  }
  buf.to_string()
}

///|
fn generate_ffi_result(ty : TypeAnnotation, self_ty : String) -> String {
  match ty {
    Option(ty) => "\{jmop_nullable}[\{generate_ffi_type_annotation(ty)}]"
    Constr(Ident(name="Self"), []) => self_ty
    _ => generate_ffi_type_annotation(ty)
  }
}

///|
fn generate_args_name(
  arguments : Map[String, ParamDirective],
) -> (String?, String) {
  let ins_name = arguments.get("self").map(d => d.name())
  arguments.remove("self")
  let args = StringBuilder::new()
  let mut first = true
  for _, arg in arguments {
    if first {
      first = false
    } else {
      args.write_string(", ")
    }
    args.write_string(arg.name())
  }
  (ins_name, args.to_string())
}

///|
fn generate_glude_error_handling(
  glude : String,
  throws : Array[String],
  source_loc : String?,
) -> String {
  if !throws.is_empty() && source_loc is Some(name) {
    "\{jmop_convert_error}(\{name}, fn(){\{glude}})"
  } else {
    // TODO: report error if throws is not empty but source_loc is None
    glude
  }
}

///|
fn generate_patch(directives : List[Directive]) -> Array[Patch] {
  let patches = []
  for directive in directives {
    match directive {
      Method(name~, ty~, arguments~, result~, throws~, hole~, source_loc~) => {
        let ffi_name = "ffi_\{ty}_\{name}"
        let (ins_name, args_name) = generate_args_name(arguments)
        guard ins_name is Some(ins_name)  // TODO: handle error case
        let glude_args = generate_glude_args(arguments)
        let glude =
          $|\{ffi_name}(self, \{glude_args})
        let glude = generate_glude_error_handling(glude, throws, source_loc)
        let ffi_result = generate_ffi_result(result, ty)
        let ffi_params = generate_ffi_params(arguments)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn \{ffi_name}(ins : \{ty}, \{ffi_params}) -> \{ffi_result} =
          $|   "(\{ins_name},\{args_name}) => \{ins_name}.\{name}(\{args_name})" 
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Constructor(ty~, arguments~, throws~, hole~, source_loc~) => {
        let ffi_name = "ffi_\{ty}_new"
        let (_, args_name) = generate_args_name(arguments)
        let ffi_params = generate_ffi_params(arguments)
        let glude_args = generate_glude_args(arguments)
        let glude =
          $|\{ffi_name}(\{glude_args})
        let glude = generate_glude_error_handling(glude, throws, source_loc)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn \{ffi_name}(\{ffi_params}) -> \{ty} = 
          $|    "(\{args_name}) => new \{ty}(\{args_name})" 
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Getter(ty~, name~, result~, throws~, hole~, source_loc~) => {
        let glude =
          $|ffi_\{ty}_get_\{name}(self)
        let glude = generate_glude_error_handling(glude, throws, source_loc)
        let ffi_result = generate_ffi_result(result, ty)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}) -> \{ffi_result}
          $|  = "(ins) => ins.\{name}" 
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Setter(ty~, name~, value~, throws~, hole~, source_loc~) => {
        // TODO: handle error case
        guard value is Some(value) else {
          abort("setter must have value parameter")
        }
        let ffi_params = generate_ffi_params({ "value": value })
        let glude =
          $|ffi_\{ty}_set_\{name}(self, \{value.name()})
        let glude = generate_glude_error_handling(glude, throws, source_loc)
        let ffi =
          #|#webgen.generated
          $|extern "js" fn ffi_\{ty}_get_\{name}(ins : \{ty}, \{ffi_params}) = 
          $|    "(ins) => ins.\{name}" 
        patches..push(Replace(hole, glude))..push(Append(ffi))
      }
      Generated(hole~) => patches.push(Remove(hole))
      Type(name~, hole~) => {
        let typedef =
          #|#webgen.ty
          $|struct \{name} (\{jmop_object})
        patches.push(Replace(hole, typedef))
      }
      Function(..) => ()
    }
  }
  patches
}

///|
fn snake_to_small_camel(s : StringView) -> String {
  // TODO: verify input string
  let parts = s.split("_").collect()
  let buf = StringBuilder::new()
  for i, part in parts {
    if i == 0 {
      buf.write_string(part.to_lower().to_string())
    } else {
      buf
      ..write_string(try! part[0:1].to_upper().to_string())
      ..write_string(try! part[1:].to_lower().to_string())
    }
  }
  buf.to_string()
}

///|
test "snake_to_small_camel" {
  inspect(snake_to_small_camel("hello_world"), content="helloWorld")
  inspect(snake_to_small_camel("Hello_World_one"), content="helloWorldOne")
  inspect(snake_to_small_camel("hello"), content="hello")
  inspect(snake_to_small_camel("h"), content="h")
  inspect(snake_to_small_camel(""), content="")
}

///|
fn remove_block_line(source : String) -> String {
  let lines = source.split("\n").collect()
  let filtered = lines.filter_map(fn(line) {
    if line.has_prefix("///|") {
      None
    } else {
      Some(line)
    }
  })
  filtered.join("\n")
}

///|
fn apply_patch0(source : String, patches : Array[Patch]) -> String {
  let appended = []
  let replacements = []
  for patch in patches {
    match patch {
      Replace(loc, s) =>
        replacements.push(
          multiline(
            (loc.start.lnum - 1, loc.start.column() - 1),
            (loc.end.lnum - 1, loc.end.column() - 2),
            s,
          ),
        )
      Remove(loc) =>
        replacements.push(
          multiline(
            (loc.start.lnum - 1, loc.start.column() - 1),
            (loc.end.lnum - 1, loc.end.column() - 2),
            "",
          ),
        )
      Append(text) => appended.push(text)
    }
  }
  let buf = StringBuilder::new(size_hint=source.length() * 2)
  buf.write_string(splice(source, replacements))
  for text in appended {
    buf.write_string("\n")
    buf.write_string(text)
  }
  buf.to_string()
}