
require:
   .util ->
      tuck, identify, next-id, ID, FORKID, VERSION, SOURCE

provide:
   make-struct, struct



make-struct{name, fields, methods} =
   cid = Symbol{name}

   make{values, mutable = false} =
      rval = Object.create{proto}
      items{fields} each {name, settings} ->
         v = if{Object.has-property.call{values, name}, values[name], settings.default}
         rval[name] = v

      identify{rval}
      tuck{rval, FORKID, rval[ID]}
      tuck{rval, VERSION, 1}

      if not mutable and Object.freeze:
         Object.freeze{rval}
      else:
         rval

   make.field-names = keys{fields}
   make.method-names = keys{methods}
   make.struct-name = name
   make.to-string{} = '{name}\{{this.field-names.join{", "}}\}'

   proto = Object.create{null}
   items{methods} each {name, method} ->
      proto[name] = method

   ;; proto.validate-field{name, value} =
   ;;    settings = fields[name]
   ;;    if settings.validate:
   ;;       try:
   ;;          settings.validate{value}
   ;;       catch err:
   ;;          throw E.struct{'''Error setting '{name}': {err.message}''', err}
   ;;    else:
   ;;       value

   Object.define-property{proto, VERSION} with {
      value = 1
      writable = true
      enumerable = false
      configurable = true
   }

   ;; proto.validate or= {} -> this

   Object.define-property{proto, "constructor"} with {
      value = make
      enumerable = false
   }

   make.proto = proto

   make["::check"]{match} =
      {constructor => === make} -> true
      else -> false

   make


macro{make-struct} struct{`{^name, ^body}`} =
   #symbol{sname} or #value{sname} = name
   #multi! #multi{*stmts} = body

   ms = @deps["make-struct"]

   fields = #data{}
   methods = `methods`
   results = #splice{
      `methods = {=}`
   }
   stmts each match stmt ->
      `^lhs = ^rhs` or lhs and rhs is null ->
         pc = @PatternCompiler{lhs, @, opt} where opt =
            @pattern_handlers.build_object & {
               assign{#symbol{@camelCase! v} or #value{v}, value} =
                  #do{#assign{`[^methods][^=v]`, value}}
               declare{vars} = {}
               wrap_target{match} =
                  `^args -> ^body` ->
                     {env => other_env} = body
                     it = #symbol{"@"} & {env = other_env}
                     it2 = #symbol{"self"} & {env = other_env}
                     `_lambda{
                        ^args
                        splice: [let ^it = this, let ^it2 = this]
                        ^body
                        ^=null
                        ^=false
                     }`
                  other ->
                     other
               success{x} =
                  #multi{}
            }
         pc.compile{}

         match pc:
            {arguments => {}, vars => match} ->
               {v} and match is rhs ->
                  do: tr = `validate{x} = [^lhs = x, ^v]`
                  null? ->
                     fields.push with `^v = {^tr}`
                  else ->
                     fields.push with `^v = {^tr, default = ^rhs}`
               else ->
                  throw E.syntax.struct with
                     '''Pattern '{@gettext{lhs}}' should define exactly one variable.'''
                     node = lhs
            {=> arguments} when rhs == null ->
               throw E.syntax.struct with
                  '''Missing method body for: '{@gettext{lhs}}' '''
                  node = lhs
            {=> arguments} ->
               results.push with pc.extract_from_rhs{rhs}

   results.push with `^name = [^ms]{^=sname, ^fields, methods}`
   results

