/* All draw in screen space */
var Shape = {
  circle(pos, radius, color) { cmd(115, pos, radius, color); },
  
  point(pos,size,color) {
    color ??= Color.blue;
    Shape.circle(pos,size,color);
  },

  /* size here is arm length - size of 2 is 4 height total */
  cross(pos, size, color, angle) {
    angle ??= 0;
    color ??= Color.red;
    var a = [
      pos.add([0,size]),
      pos.add([0,-size])
    ];
    var b = [
      pos.add([size,0]),
      pos.add([-size,0])
    ];
    
    Shape.line(a,color);
    Shape.line(b,color);
  },
  
  arrow(start, end, color, wingspan, wingangle) {
    color ??= Color.red;
    wingspan ??= 4;
    wingangle ??=10;
    
    var dir = end.sub(start).normalized();
    var wing1 = [
      Vector.rotate(dir, wingangle).scale(wingspan).add(end),
      end
    ];
    var wing2 = [
      Vector.rotate(dir,-wingangle).scale(wingspan).add(end),
      end
    ];
    Shape.line([start,end],color);
    Shape.line(wing1,color);
    Shape.line(wing2,color);
  },
  
  poly(points, color) { cmd_points(0,points,color); },
  
  box(pos, wh, color) {
    color ??= Color.white;
    cmd(53, pos, wh, color);
  },

  line(points, color, type, thickness) {
    thickness ??= 1;
    type ??= 0;
    color ??= Color.white;
    
    switch (type) {
      case 0:
        cmd(83, points, color, thickness);
    }
  },
};

var Debug = {
  fn_break(fn, obj) {
    if (typeof fn !== 'function') return;
    obj ??= globalThis;
    
    var newfn = function() {
      console.log("broke");
      fn();
    };
    obj[fn.name] = newfn;    
  },

  draw_grid(width, span, color) {
    color = color ? color : Color.green;
    cmd(47, width, span, color);
  },
  
  coordinate(pos, size, color) { GUI.text(JSON.stringify(pos.map(p=>Math.round(p))), pos, size, color); },

  boundingbox(bb, color) {
    color ??= Color.white;
    cmd_points(0, bb2points(bb), color);
  },

  numbered_point(pos, n, color) {
    color ??= Color.white;
    Shape.point(pos, 3, color);
    GUI.text(n, pos.add([0,4]), 1, color);
  },

  phys_drawing: false,
  draw_phys(on) {
    this.phys_drawing = on;
    cmd(4, this.phys_drawing);
  },

  draw_obj_phys(obj) {
    cmd(82, obj.body);
  },

  register_call(fn, obj) {
    Register.debug.register(fn,obj);
  },

  gameobject(go) { cmd(15, go.body); },

  draw_bb: false,
  draw_gizmos: false,
  draw_names: false,

  draw() {
    if (this.draw_bb)
      Game.all_objects(function(x) { Debug.boundingbox(x.boundingbox(), Color.Debug.boundingbox.alpha(0.05)); });

    if (Game.paused()) GUI.text("PAUSED", [0,0],1);

    if (this.draw_gizmos)
      Game.all_objects(function(x) {
        if (!x.icon) return;
        GUI.image(x.icon, world2screen(x.pos));
      });

    if (this.draw_names)
      Game.all_objects(function(x) {
        GUI.text(x, world2screen(x.pos).add([0,32]), 1, Color.Debug.names);
      });

    if (Debug.Options.gif.rec) {
      GUI.text("REC", [0,40], 1);
      GUI.text(Time.seconds_to_timecode(Time.time - Debug.Options.gif.start_time, Debug.Options.gif.fps), [0,30], 1);
    }

    GUI.text(Game.playing() ? "PLAYING"
                         : Game.stepping() ?
			 "STEP" :
			 Game.paused() ?
			 "PAUSED; EDITING" : 
			 "EDIT", [0, 0], 1);
  },
};

Debug.assert = function(b, str)
{
  str ??= "";
  
  if (!b) {
    console.error(`Assertion failed. ${str}`);
    Game.quit();
  }
}

Debug.Options = { };
Debug.Options.Color = {
  set trigger(x) { cmd(17,x); },
  set debug(x) { cmd(16, x); },
};

var Gizmos = {
  pick_gameobject_points(worldpos, gameobject, points) {
    var idx = Math.grab_from_points(worldpos, points.map(gameobject.this2world,gameobject), 25);
    if (idx === -1) return undefined;
    return idx;
  },
};

var Profile = {
  tick_now() { return cmd(127); },
  ns(ticks) { return cmd(128, ticks); },
  us(ticks) { return cmd(129, ticks); },
  ms(ticks) { return cmd(130, ticks); },
  best_t(ns) {
    var e = ns;
    var qq = 'ns';
    if (e > 1000) {
      e /= 1000;
      qq = 'us';
      if (e > 1000) {
        e /= 1000;
	qq = 'ms';
      }
    }
    return {
      time: e,
      unit: qq
    };
  },
  cpu(fn, times, q) {
    times ??= 1;
    q ??= "unnamed";
    var start = Profile.tick_now();
    for (var i = 0; i < times; i++)
      fn();
      
    var elapsed = Profile.tick_now() - start;
    var avgt = Profile.best_t(elapsed/times);
    var totalt = Profile.best_t(elapsed);

    console.say(`Profile [${q}]: ${avgt.time.toFixed(3)} ${avgt.unit} average [${totalt.time.toFixed(3)} ${totalt.unit} for ${times} loops]`);
  },

  get fps() { return sys_cmd(8); },
};



Profile.test = {
  barecall() { profile(0); },
  unpack_num(n) { profile(1,n); },
  unpack_array(n) { profile(2,n); },
  pack_num() { profile(3); },
  pack_string() { profile(6); },
  unpack_string(s) { profile(4,s); },
  unpack_32farr(a) { profile(5,a); },
};

Profile.cpu.doc = `Output the time it takes to do a given function n number of times. Provide 'q' as "ns", "us", or "ms" to output the time taken in the requested resolution.`;

/* These controls are available during editing, and during play of debug builds */
var DebugControls = {};
DebugControls.toString = function() { return "Debug"; };
DebugControls.inputs = {};
DebugControls.inputs.f1 = function () { Debug.draw_phys(!Debug.phys_drawing); };
DebugControls.inputs.f1.doc = "Draw physics debugging aids.";
//DebugControls.inputs.f3 = function() { Debug.draw_bb = !Debug.draw_bb; };
//DebugControls.inputs.f3.doc = "Toggle drawing bounding boxes.";
DebugControls.inputs.f4 = function() {
//  Debug.draw_names = !Debug.draw_names;
//  Debug.draw_gizmos = !Debug.draw_gizmos;
};
DebugControls.inputs.f4.doc = "Toggle drawing gizmos and names of objects.";

Debug.Options.gif = {
  w: 640, /* Max width */
  h: 480, /* Max height */
  stretch: false, /* True if you want to stretch */
  cpf: 4,
  depth: 16,
  file: "out.gif",
  rec: false,
  secs: 6,
  start_time: 0,
  fps: 0,
  start() {
    var w = this.w;
    var h = this.h;
    if (!this.stretch) {
      var win = Window.height / Window.width;    
      var gif = h/w;
      if (gif > win)
        h = w * win;
      else
        w = h / win;
    }

    cmd(131, w, h, this.cpf, this.depth);
    this.rec = true;
    this.fps = (1/this.cpf)*100;
    this.start_time = Time.time;

    timer.oneshot(this.stop.bind(this), this.secs, this, true);
  },

  stop() {
    if (!this.rec) return;
    cmd(132, this.file);
    this.rec = false;
  },
};

DebugControls.inputs.f8 = function() {
  var now = new Date();
  Debug.Options.gif.file = now.toISOString() + ".gif";
  Debug.Options.gif.start();
};
DebugControls.inputs.f9 = function() {
  Debug.Options.gif.stop();
}

DebugControls.inputs.f10 = function() { Time.timescale = 0.1; };
DebugControls.inputs.f10.doc = "Toggle timescale to 1/10.";
DebugControls.inputs.f10.released = function () { Time.timescale = 1.0; };
DebugControls.inputs.f12 = function() { GUI.defaults.debug = !GUI.defaults.debug; Log.warn("GUI toggle debug");};
DebugControls.inputs.f12.doc = "Toggle drawing GUI debugging aids.";

DebugControls.inputs['M-1'] = Render.normal;
Render.normal.doc = "Render mode for enabling all shaders and lighting effects.";
DebugControls.inputs['M-2'] = Render.wireframe;
Render.wireframe.doc = "Render mode to see wireframes of all models.";

DebugControls.inputs['C-M-f'] = function() {};
DebugControls.inputs['C-M-f'].doc = "Enter camera fly mode.";

var Time = {
  set timescale(x) { cmd(3, x); },
  get timescale() { return cmd(121); },
  set updateMS(x) { cmd(6, x); },
  set physMS(x) { cmd(7, x); },
  set renderMS(x) { cmd(5, x); },

  get time() { return cmd(133); },

  seconds_to_timecode(secs, fps)
  {
    var s = Math.trunc(secs);
    secs -= s;
    var f = Math.trunc(fps * secs);
    return `${s}:${f}`;
  },

  pause() {
    Time.stash = Time.timescale;
    Time.timescale = 0;
  },

  play() {
    if (!Time.stash) {
      Log.warn("Tried to resume time without calling Time.pause first.");
      return;
    }
    Time.timescale = Time.stash;
  },
};

Time.doc = {};
Time.doc.timescale = "Get and set the timescale. 1 is normal time; 0.5 is half speed; etc.";
Time.doc.updateMS = "Set the ms per game update.";
Time.doc.physMS = "Set the ms per physics update.";
Time.doc.renderMS = "Set the ms per render update.";
Time.doc.time = "Seconds elapsed since the game started.";
Time.doc.pause = "Pause the game by setting the timescale to 0; remembers the current timescale on play.";
Time.doc.play = "Resume the game after using Time.pause.";

Player.players[0].control(DebugControls);
Register.gui.register(Debug.draw, Debug);

var console = Object.create(Log);
console.log = function(str)
{
  console.say(time.text(time.now(), 'yyyy-m-dd hh:nn:ss') + "  " + str);
}
console.clear = function()
{
  cmd(146);
}

var API = {};
API.doc_entry = function(obj, key)
{
  var d = obj.doc;
  var doc = "";
  var title = key;
  
  var o = obj[key];
  if (typeof o === 'undefined' && obj.impl && typeof obj.impl[key] !== 'undefined')
    o = obj.impl[key];

  var t = typeof o;
  if (t === 'object' && Array.isArray(o)) t = 'array';

  if (t === 'function') {
    title = o.toString().tofirst(')') + ")";
    if (o.doc) doc = o.doc;
    t = "";
  }
  if (t === 'undefined') t = "";

  if (t) t = "**" + t + "**";

  if (!doc) {
    if (d && d[key]) doc = d[key];
    else return "";
  }

  return `### \`${title}\`
${t}
${doc}
`;
}

API.print_doc =  function(name)
{
  var obj = eval(name);
  if (!obj.doc) {
    Log.warn(`Object has no doc sidecar.`);
    return;
  }

  var mdoc = "# " + name + " API #\n";
  if (obj.doc?.doc) mdoc += obj.doc.doc + "\n";
  else if (typeof obj.doc === 'string') mdoc += obj.doc + "\n";
  for (var key in obj) {
    if (key === 'doc') continue;
    if (key === 'toString') continue;
    mdoc += API.doc_entry(obj, key);
  }

  return mdoc;
}
