/*
 * Title: coil.c
 * Author: Jed Marti KI7NNP
 * Description: Generate openSCAD coil forms. This code is free for any use.
 * Revision History: (revised Sun Mar 12 08:05:37 2023)
 *   Thu Mar 16 10:23:57 2023 - JBM Clean up for website.
 *   Sat Dec  9 07:17:35 2023 - JBM Add end overhang.
 */


#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <math.h>

#ifdef _MBCS
  // Make Microsoft useful.
  #define strcasecmp _stricmp
  #define M_PI 3.14159265358979323846
#endif

#include "wire.h"


#define VERSION 3
#define RELEASE 1
#define PATCH   0

// The wire table (see wire.h).
WireTable;

// Number of cylinders/turn.
int32_t cPT = 18;

// Wire diameter in inches, or gauge.
double wD = 0.028;
int32_t gG = 22;

// Cylinder wall thickness in inches.
double wT = 0.125;

// Number of faces in wire indents.
int32_t cW = 9;

// Number of coil faces (inside and out).
int32_t cF = 90;

// The layer thickness. Defaults to 0.3mm
double layer = 0.011811;

// End overhang in inches.
double eOH = 0.1;

// Radius, length, turns are undefined and must be defined  from the command line.
double rad = -1.0;
double len = -1.0;
int32_t turns = -1;


int32_t nextValue(int32_t i, int32_t argc, char *cmd, char *ty)
/*     Scan down the command list. Give an error if an argument wants a value and it isn't there.
 * Parameters:
 *   i: The current parameter index.
 *   argc: from main, the argument count.
 *   cmd: The command we're looking for.
 *   ty: The type of value (a string) we're looking for (int, float, string)
 * External References: none
 * Returned value: the command index + 1 - where the actual value is.
 * Errors detected: Missing argument.
 */
{
  if (++i >= argc)
  {
    fprintf(stderr, "%s missing %s value\n", cmd, ty);
    exit(-1);
  }
  return i;
}



int32_t intValue(char *cmd, char *arg, int32_t lowv, int32_t highv)
/*     Called by the intParam macro. Check for an integer argument that sits between the high
 *  and low values. 
 * Parameters:
 *   cmd: command line parameter name we're checking for.
 *   arg: The argument to check.
 *   lowv: The smallest value allowed.
 *   highv: The largest value allowed.
 * External References: none
 * Returned values: The integer (32 bit) value.
 * Errors detected: parameter not in range.
 */
{
  int32_t v;
  v = atoi(cmd);
  if (v < lowv || v > highv)
  {
    fprintf(stderr, "%s is %d, must be in range %d to %d\n", arg, v, lowv, highv);
    exit(-1);
  }
  return v;
}



double floatValue(char *cmd, char *arg, double lowv, double highv)
/*     Called by the floatParam macro. Check for a floating-point argument that sits between the 
 *   high and low values. 
 * Parameters:
 *   cmd: command line parameter name we're checking for.
 *   arg: The argument to check.
 *   lowv: The smallest value allowed.
 *   highv: The largest value allowed.
 * External References: none
 * Returned values: The double precision value.
 * Errors detected: parameter not in range.
 */
{
  double v;
  v = atof(cmd);
  if (v < lowv || v > highv)
  {
    fprintf(stderr, "%s is %g, must be in range %g to %g\n", arg, v, lowv, highv);
    exit(-1);
  }
  return v;
}


// Macros for interpreting command line parameters.
#define intParam(nme, ty, var, lowv, highv) \
(strcasecmp(argv[i], nme) == 0) \
{ \
  i = nextValue(i, argc, nme, ty); \
  var = intValue(argv[i], nme, lowv, highv); \
}

#define floatParam(nme, ty, var, lowv, highv) \
(strcasecmp(argv[i], nme) == 0) \
{ \
  i = nextValue(i, argc, nme, ty); \
  var = floatValue(argv[i], nme, lowv, highv); \
}



void scanCmdLine(int32_t argc, char *argv[])
/*     Scan the command line for arguments and their values - set the globals.
 * Parameters:
 *   argc, arg[]: from main with standard definitions.
 * External References: see all above.
 * Returned values: none
 * Errors detected: Missing parameters, unknown wire gauge.
 */
{
  int32_t i;

  // Scan command line arguments. Use macros and functions above.
  for (i = 2; i < argc; ++i)
  {
    if intParam("-cylindersPerTurn", "integer", cPT, 3, 360)
    else if floatParam("-wireSize", "float", wD, 0.0001, 0.25)
    else if intParam("-gauge", "integer", gG, 14, 32)
    else if floatParam("-wallThickness", "float", wT, 0.01, 1.0)
    else if intParam("-wireIndentFaces", "integer", cW, 3, 90)
    else if intParam("-formFaces", "integer", cF, 3, 360)
    else if floatParam("-radius", "float", rad, 0.05, 5.0)
    else if floatParam("-length", "float", len, 0.05, 10.0)
    else if floatParam("-layer", "float", layer, 0.001, 0.04)
    else if floatParam("-end", "float", eOH,  0.001, 1.0)
    else if intParam("-turns", "int", turns, 1, 500)
    else
    {
      fprintf(stderr, "Option %s unknown\n", argv[i]);
      exit(-1);
    }
  }

  // Verify parameters are present.
  if (rad == -1.0)
  {
    fprintf(stderr, "-radius parameter missing\n");
    exit(-1);
  }
  if (len == -1.0)
  {
    fprintf(stderr, "-length parameter missing\n");
    exit(-1);
  }
  if (turns == -1)
  {
    fprintf(stderr, "-turns parameter missing\n");
    exit(-1);
  }

  // Now fiddle gauge.
  if (gG != 22)
  {
    for (i = 0; i < sizeof(W) / sizeof(WireData_t); ++i)
      if (W[i].gauge == gG)
      {
        wD = W[i].enamel;
        return;
      }
    fprintf(stderr, "Wire gauge %d unknown\n", gG);
  }
        
}
      

// Useful function.
double sqr(double x) { return x * x; }

// OpenSCAD needs degrees.
#define rad2deg (180.0 / M_PI)


int main(int argc, char *argv[])
{
  FILE    *fh;
  time_t  ct;
  char    *ctc;
  double  theta;
  double  w;
  double  cl;
  double  dz;
  int32_t i;
  double  phi;
  double  wR;
  char    msg[10];;

  // Help if no command line argumets.
  if (argc < 2)
  {
    fprintf(stderr, "KI7NNP coil V%d.%02d.%03d\n", VERSION, RELEASE, PATCH);
    fprintf(stderr, "Usage:\n\
    coil file-out [required, ...] [options, ...]\n\
optional:\n\
  -cylindersPerTurn <int:%d>  Make larger for big radii\n\
  -wireSize <float:%g>        Make larger than listed\n\
  -gauge <int:%d>             Use table instead of -wireSize\n\
  -wallThickness <float:%g>   Must not be larger than radius\n\
  -wireIndentFaces <int:%d>   Make larger for heavy gauge\n\
  -formFaces <int:%d>         Make smaller for small radii\n\
  -layer <float:%g>           Printer layer thickness.\n\
  -end <float:%g>             Coil end extension.\n\
required:\n\
  -radius <float> - coil radius in inches\n\
  -length <float> - coil length in inches\n\
  -turns <int>     - Number of turns\n", cPT, wD, gG, wT, cW, cF, layer, eOH);
    fprintf(stderr, "Known #guage: copper (enameled or -1)\n");
    for (i = 0; i < sizeof(W) / sizeof(WireData_t); ++i)
      fprintf(stderr, "   #%s: %g\" (%g\")\n", W[i].name, W[i].bare, W[i].enamel);
    exit(0);
  }

  // Missing file name is common so check this. Then open the output file.
  if ((argv[1])[0] == '-')
  {
    fprintf(stderr, "%s is not a valid file name\n", argv[1]);
    exit(-1);
  }
  if ((fh = fopen(argv[1], "wb")) == NULL)
  {
    fprintf(stderr, "Couldn't open %s for output\n", argv[1]);
    exit(-1);
  }

  // Scan input parameters.
  scanCmdLine(argc, argv);

  // Now check the wire size against the layer thickness.
  if (wD / 2.0 < layer)
  {
    wD = 2.0 * layer;
    fprintf(stderr, "Wire diameter too small for printer, corrected to %g inches\n", wD);
  }

  if (wD * turns > (len + eOH))
  {
    fprintf(stderr, "Wire diameter %g too large for length %g by %g inches\n", wD, len,
            (wD * turns) - len);
    exit(-1);
  }

  // The date created and parameters used to generate the form.
  ct = time(0);
  ctc = ctime(&ct);
  ctc[strlen(ctc) - 1] = 0;
  fprintf(fh, "//  Coil cylindersPerTurn %d\n", cPT);
  fprintf(fh, "//  Coil wireSize %g\"\n", wD);
  fprintf(fh, "//  Coil wallThickness %g\"\n", wT);
  fprintf(fh, "//  Coil wireIndentFaces %d\n", cW);
  fprintf(fh, "//  Coil formFaces %d\n", cF);
  fprintf(fh, "//  Coil end overhang %g\n", eOH);
  fprintf(fh, "//  Coil radius %g\"\n", rad);
  fprintf(fh, "//  Coil length %g\"\n", len);
  fprintf(fh, "//  Coil turns  %d\n", turns);
  fprintf(fh, "//  Coil extensions %g\n", eOH);

  // PLA expansion. Evidently not needed.
  //  rad *= 1.02;
  //  len *= 1.02;

  // prolog.
  fprintf(fh, "\nscale([25.4, 25.4, 25.4])\n");
  fprintf(fh, "difference() {\n");
  fprintf(fh, "  // The master coil cylinder.\n");
  fprintf(fh, "  cylinder($fn=%d, r=%g, h=%g);\n", cF, rad, len + 2.0 * eOH);

  fprintf(fh, "\n  // The hole through the center\n");
  fprintf(fh, "  translate ([0,0,-0.1]) cylinder($fn=%d, r=%g, h=%g);\n", cF, rad - wT, len + 0.5);

  fprintf(fh, "\n  // The two wire holes.\n");
  fprintf(fh, "  translate([%g,0,%g]) rotate([0,90,0]) cylinder($fn=%d,h=%g,d=%g);\n",
          0.0, eOH, cW, 2.0*rad, 1.5 * wD);
  fprintf(fh, "  translate([%g,0,%g]) rotate([0,90,0]) cylinder($fn=%d, h=%g, d=%g);\n",
          0.0, len + eOH, cW, 2.0*rad, 1.5 * wD);

  // The wire indents.
  theta = atan2(len, turns * 2 * rad * M_PI) * rad2deg;
  printf("  theta = %g\n", theta);

  // The wire length.
  w = sqrt(sqr(len) + sqr(turns * 2 * rad * M_PI));
  printf("  wire length = %g\n", w);

  // Cylinder length for each cylinder in the helix.
  cl = 1.2 * (w / (turns * cPT));
  printf("  Cylinder length %g inches\n", cl);

  // Z offset for each cylinder.
  dz = len / (turns * cPT);
  printf("  Cylinder offset Z %g inches\n", dz);

  // Offset
  wR = wD / 2.0;

  fprintf(fh, "\n  // The turn slots.\n");
  for (i = 0; i <= turns * cPT; ++i)
  {
    phi = (i * (360.0 / cPT)) / rad2deg;
    fprintf(fh, "  translate([%g,%g,%g]) ", rad * cos(phi), rad * sin(phi),
            i * dz + eOH);
    fprintf(fh, "rotate([0,0,%g]) ", phi * rad2deg);
    fprintf(fh, "rotate([%g-90,0,0]) translate([0,0,%g]) cylinder($fn=%d,d=%g,h=%g);\n",
              theta, cl / -2.0, cW, wD, cl);
  };
  
  fprintf(fh, "};\n");
  fclose(fh);

}



// EOF
