/*
   Allows structs to be copied and assigned inline.

   For example, copying a Vector3 while changing a single value:
      old := Vector3.{ 10, 20, 30 };  // 10,  20, 30
      new := with(old, .{ y = -20 }); // 10, -20, 30
*/
with :: (old: $T, $fields: Code, location := #caller_location) -> T
#modify { return T.(*Type_Info).type == .STRUCT, "with can only be used on structs"; }
#expand {
   #insert,scope() -> string {
      using compiler;

      ensure :: (cond: bool, message: string, args: ..Any, loc := location) {
         if !cond compiler_report(basic.tprint(message, ..args), loc);
      }

      b: basic.String_Builder;
      root := compiler_get_nodes(fields).(*Code_Literal);
      ensure(root.kind == .LITERAL, "argument must be a struct literal");
      ensure(root.value_type == .STRUCT, "argument must be a struct literal");

      t_info := T.(*Type_Info_Struct);

      // Ensure the literal we were given is of type T so this operator is more predictable.
      // i.e. disallowing Vector3.{} | SomeRandomType.{ x = 10 };
      n_type := root.struct_literal_info.type_expression;
      if n_type != null {
         tmp: basic.String_Builder;
         pp.print_expression(*tmp, n_type);
         n_typename := basic.builder_to_string(*tmp);
         ensure(n_type.result == t_info, "mismatched types, % vs. %", t_info.name, n_typename);
      }

      // @note(judah): if the value we're trying to copy is a constant,
      // we have to create a local so we can assign to the fields.
      // Doing this allows this usecase: 'with(Default_Entity, .{ kind = .Player })'
      receiver := "old";
      #if is_constant(old) {
         receiver = "copy";
         basic.print_to_builder(*b, "% := old;\n", receiver);
      }

      for root.struct_literal_info.arguments {
         op    := it.(*Code_Binary_Operator);
         ident := op.left.(*Code_Ident);

         ensure(op.kind == .BINARY_OPERATOR, "copy-assign requires named fields", loc = make_location(op));
         ensure(op.operator_type == #char "=", "copy-assign requires named field assignment", loc = make_location(op));
         ensure(ident.kind == .IDENT, "must be an identifier", loc = make_location(op));

         // Catch any incorrect field assignments before the compiler does for better error reporting
         exists := false;
         for t_info.members if it.name == ident.name exists = true;
         ensure(exists, "field % does not exist within %", ident.name, t_info.name, loc = make_location(ident));

         // receiver.field = value;
         basic.append(*b, receiver);
         basic.append(*b, ".");
         pp.print_expression(*b, op);
         basic.append(*b, ";\n");
      }

      basic.print_to_builder(*b, "return %;\n", receiver);
      return basic.builder_to_string(*b);
   }
}

// @note(judah): I like doing this with an operator, but your mileage may vary
operator | :: with;


/*
   Creates a named block that can exit early (via 'break' or 'continue').

   This mostly replaces the case where you'd like to jump to
   the end of a scope based on some logic within. Without
   gotos, this is the next best thing.

   Usage:
      // within a loop
      for this_block() { // this is named 'block' by default
         if !moving break;
         // do movement here
      }
      for this_block("render_player") {
         if invisible break render_player;
         // do rendering here
      }
*/
this_block :: ($name: string = Default_Name) -> Named_Block(name) #expand { return .{}; }

/*
   Drop-in loop unrolling macro.

   Usage:
      for unroll(5) {
         // duplicates this body 5 times exactly
      }

      known_size: [3]float;
      for unroll(known_size) {
         // duplicates this body 3 times exactly
      }

      var_size: []float;
      for unroll(var_size) {
         // duplicates this body a set number of times,
         // falling back to a regular for loop to handle
         // the remaining iterations.
      }
*/
unroll :: ($count: int) -> Unrolled_Loop(count) { return .{}; }
unroll :: (arr: [$N]$T) -> Unrolled_Loop(N, T)  { return .{ array = arr }; }
unroll :: (arr: []$T)   -> Unrolled_Loop(-1, T) { return .{ array = arr }; }

// Call #c_call procedures inline with the current context: 'c_call(some_c_call_proc(10, 20))'
c_call :: (call: Code) #expand {
   push_context context { #insert,scope(call) call; }
}

// Call #c_call procedures inline with a custom context: 'c_call(some_c_call_proc(10, 20), c_context)'
c_call :: (call: Code, ctx: #Context) #expand {
   push_context ctx { #insert,scope(call) call; }
}

// @note(judah): for_expansions have to be exported

for_expansion :: (v: *Named_Block, code: Code, _: For_Flags) #expand {
   #insert #run basic.tprint(#string END
      for `%: 0..0 {
         `it       :: #run mem.zero_of(void);
         `it_index :: #run mem.zero_of(void);
         #insert,scope(code) code;
      }
   END,
   // @note(judah): guards against calling this_block with
   // an empty string which results in weird error messages.
   ifx v.NAME.count != 0 v.NAME else Default_Name);
}

for_expansion :: (loop: *Unrolled_Loop, body: Code, flags: For_Flags, loc := #caller_location) #expand {
   // runtime unroll
   #if loop.N == -1 {
      for #v2 <=(flags & .REVERSE == .REVERSE) i: 0..loop.array.count - 1 {
         #if flags & .POINTER {
            `it := *(#no_abc loop.array[i]);
         }
         else {
            `it := #no_abc loop.array[i];
         }

         `it_index := i;
         #insert,scope(body) body;
      }
   }
   // compile-time unroll
   else {
      `it_index := 0;

      #insert -> string {
         b: basic.String_Builder;
         basic.print_to_builder(*b, "// inserted unrolled loop (N = %) at %:%\n", loop.N, loc.fully_pathed_filename, loc.line_number);

         if loop.T == void {
            basic.append(*b, "`it: int = ---;\n");
         }
         else {
            basic.append(*b, "`it: loop.T = ---;\n");
         }

         for #v2 <=(flags & .REVERSE == .REVERSE) 0..loop.N - 1 {
            basic.append(*b, "{\n");

            if loop.T == void {
               basic.print_to_builder(*b, "\tit = %;\n", it);
            }
            else {
               #if flags & .POINTER {
                  basic.print_to_builder(*b, "\tit = *(#no_abc loop.array[%]);\n", it);
               }
               else {
                  basic.print_to_builder(*b, "\tit = #no_abc loop.array[%];\n", it);
               }
            }

            basic.print_to_builder(*b, "\tit_index = %;\n", it);
            basic.append(*b, "\t#insert,scope(body) body;\n");
            basic.append(*b, "}\n");
         }

         return basic.builder_to_string(*b);
      }
   }
}

Default_Name :: "block";
Named_Block  :: struct(NAME: string) {}

Unrolled_Loop :: struct(N: int, T: Type = void) {
   // Only store arrays when we absolutely have to.
   #if T != void {
      // @todo(judah): because this will only be created via 'unroll',
      // should these be pointers to the underlying arrays so we don't
      // pay for a copy?
      #if N == -1 {
         array: []T = ---;
      }
      else {
         array: [N]T = ---;
      }
   }
}


#scope_file;

basic    :: #import "Basic"; // @future
pp       :: #import "Program_Print"; // @future
compiler :: #import "Compiler"; // @future


// ----------------------------------------------------------
// TESTS
// ----------------------------------------------------------

#if RUN_TESTS #run {
   test.run("this_block", (t) => {
      i := 0;

      for this_block() {
         i += 1;
         for this_block() {
            break block;
         }

         for this_block() {
            continue block;
         }

         if i == 1 {
            break block;
         }

         i += 2;
      }

      j := 0;
      for this_block("named") {
         for 0..10 {
            break;
         }

         if i != 1 {
            break named;
         }

         j = 1;
      }

      test.expect(t, i == 1, "i was %", i);
      test.expect(t, j == 1, "j was %", j);
   });

   test.run("copy assign", (t) => {
      Value :: struct {
         x: float;
         y: float;
         z: float;
      }

      a := Value.{ 10, 20, 30 };
      b := with(a, .{ x = 1, z = 1 });
      c := b | .{ y = 500 };

      test.expect(t, b.x == 1, "was %", b.x);
      test.expect(t, b.z == 1, "was %", b.z);
      test.expect(t, c.y == 500, "was %", c.y);
   });
}

