#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <float.h>
#include <error.h>
#include <errno.h>

#include "grsys.h"

#ifndef float64
#define float64 double
#endif

#ifndef true
#define true (0==0)
#endif

#ifndef false
#define false (0!=0)
#endif

typedef struct {
  float64 x, y;
  int code; // 0 = pen up; 1 = pen down
} vpNode;

float64 grsys_XMin = 0.0;
float64 grsys_XMax = 10.0;
float64 grsys_YMin = 0.0;
float64 grsys_YMax;
float64 grsys_XCenter;
float64 grsys_YCenter;
float64 grsys_RMax;
float64 grsys_Density;
FILE *grsys_plotFile;
int grsys_inGrMode;// bool

float64 xMinClip, xMaxClip, yMinClip, yMaxClip;

float64 xMinWorld, xMaxWorld;
float64 yMinWorld, yMaxWorld;
float64 xCWorld, yCWorld;
float64 xCView, yCView;
float64 fScale;
int     iwCalled = false, wbCalled = false, vbCalled = false; // bool

int grsys_plotCoord(float64 x) {
  return (int)(1000.0*x + 0.5);
}

void grsys_Move(float64 x, float64 y) {
  fprintf(stderr, "Move(%lf,%lf);\n", x, y);
}

void grsys_Draw(float64 x, float64 y) {
  fprintf(stderr, "Draw(%lf,%lf);\n", x, y);
}

Vec grsys_Rotate(Vec p, Vec c, float64 cosphi, float64 sinphi) {
  float64 dx = p.X - c.X;
  float64 dy = p.Y - c.Y;
    return (Vec){c.X + dx*cosphi - dy*sinphi,
                 c.Y + dx*sinphi + dy*cosphi};
}

void grsys_InitGr(char* filename) {
  fprintf(stderr, "InitGr(\"%s\");\n", filename);

#ifdef NEVER
  NColors = GetMaxColor() + 1;
  palette = make([]color.Color, NColors);
  for i := range palette {
    palette[i] = defaultPalette[i];
  }
  bounds := image.Rect(0, 0, ImageWidth, ImageHeight);
  canvas = image.NewPaletted(bounds, palette);

  SetRGBPalette(0, 0, 0, 63);
  SetRGBPalette(14, 63, 63, 0);
  BackGrColor = 0;
  ForeGrColor = 14;
#else
  float64 ImageHeight = 768.0;
  float64 ImageWidth = 1024.0;
#endif
  
  grsys_Density = (float64)(ImageWidth) / (grsys_XMax - grsys_XMin);
  grsys_YMax = grsys_YMin + (float64)(ImageHeight)/grsys_Density;
  grsys_XCenter = (grsys_XMin + grsys_XMax) / 2;
  grsys_YCenter = (grsys_YMin + grsys_YMax) / 2;
  if (grsys_XCenter < grsys_YCenter) {
    grsys_RMax = grsys_XCenter;
  } else {
    grsys_RMax = grsys_YCenter;
  }
  if (*filename != '\0') {
    grsys_plotFile = fopen(filename, "w");
    if (grsys_plotFile == NULL) {
      fprintf(stderr, "%s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }
    fprintf(grsys_plotFile, "IN;SP0;SC0,10000,0,%d;\n", grsys_plotCoord(grsys_YMax-grsys_YMin));
  }
  grsys_inGrMode = true;
}

void grsys_EndGr(void) {
  fprintf(stderr, "EndGr();\n");
  
  grsys_inGrMode = false;
#ifdef NEVER
  if (outside) {
    //log.SetFlags(0);
    fprintf(stderr, "Warning: attempts to draw outside the screen\n");
  }
#endif
  if (grsys_plotFile) {
    int rc = fclose(grsys_plotFile);
    if (rc) {
      fprintf(stderr, "%s\n", strerror(errno));
      exit(EXIT_FAILURE);
    }
    grsys_plotFile = NULL;
  }
  //encode("ppicg.gif");
}

void grsys_InitWindow(void) {
  fprintf(stderr, "InitWindow();\n");
  xMinWorld = FLT_MAX;
  yMinWorld = FLT_MAX;
  xMaxWorld = -FLT_MAX;
  yMaxWorld = -FLT_MAX;
  iwCalled = true;
}

void grsys_UpdateWindowBoundaries(float64 x, float64 y) {
  if (!iwCalled) {
    fprintf(stderr, "Call InitWindow before UpdateWindowBoundaries\n");
    exit(EXIT_FAILURE);
  }
  if (x < xMinWorld) {
      xMinWorld = x;
  }
  if (x > xMaxWorld) {
      xMaxWorld = x;
  }
  if (y < yMinWorld) {
      yMinWorld = y;
  }
  if (y > yMaxWorld) {
      yMaxWorld = y;
  }
  wbCalled = true;
  fprintf(stderr, "UpdateWindowBoundaries(%lf,%lf);\n", x, y);
}

void grsys_ViewportBoundaries(float64 Xmin, float64 Xmax, float64 Ymin, float64 Ymax, float64 reductionFactor) {
  fprintf(stderr, "ViewportBoundaries(%lf,%lf,%lf,%lf, %lf);\n", Xmin, Xmax, Ymin, Ymax, reductionFactor);
  if (!grsys_inGrMode) {
    fprintf(stderr, "Call InitGr before ViewportBoundaries\n");
    exit(EXIT_FAILURE);
  }
  if (!wbCalled) {
    fprintf(stderr, "Call UpdateWindowBoundaries before ViewportBoundaries\n");
    exit(EXIT_FAILURE);
  }
  xCView = (grsys_XMin + grsys_XMax) / 2;
  yCView = (grsys_YMin + grsys_YMax) / 2;
  float64 fx = (grsys_XMax - grsys_XMin) / (xMaxWorld - xMinWorld + 1e-7); // +1e-7 to prevent
  float64 fy = (grsys_YMax - grsys_YMin) / (yMaxWorld - yMinWorld + 1e-7); // division by zero
  if (fx < fy) {
    fScale = fx;
  } else {
    fScale = fy;
  }
  fScale *= reductionFactor;
  xCWorld = (xMinWorld + xMaxWorld) / 2;
  yCWorld = (yMinWorld + yMaxWorld) / 2;
  vbCalled = true;
}

float64 grsys_XViewport(float64 x) {
  fprintf(stderr, "XViewport(%lf);\n", x);
  if (!vbCalled) {
    fprintf(stderr, "Call ViewportBoundaries before XViewport\n");
    exit(EXIT_FAILURE);
  }
  return xCView + fScale*(x-xCWorld);
}

float64 grsys_YViewport(float64 y) {
  fprintf(stderr, "YViewport(%lf);\n", y);
  if (!vbCalled) {
    fprintf(stderr, "Call ViewportBoundaries before YViewport\n");
    exit(EXIT_FAILURE);
  }
  return yCView + fScale*(y-yCWorld);
}

void grsys_SetClipBoundaries(float64 x1, float64 x2, float64 y1, float64 y2) {
  xMinClip = x1;
  xMaxClip = x2;
  yMinClip = y1;
  yMaxClip = y2;
}

static int clipCode(float64 x, float64 y) {
  int c;
  if (x < xMinClip) {
    c |= 8;
  } else if (x > xMaxClip) {
    c |= 4;
  }
  if (y < yMinClip) {
    c |= 2;
  } else if (y > yMaxClip) {
    c |= 1;
  }
  return c;
}

void grsys_ClipDraw(float64 xP, float64 yP, float64 xQ, float64 yQ) {
  int cP = clipCode(xP, yP);
  int cQ = clipCode(xQ, yQ);
  while ((cP | cQ)) {
    if (cP & cQ) {
        return;
    }
    float64 dx = xQ - xP;
    float64 dy = yQ - yP;
    if (cP) {
      if (cP & 8) {
        yP += (xMinClip - xP) * dy / dx;
        xP = xMinClip;
      } else if (cP & 4) {
        yP += (xMaxClip - xP) * dy / dx;
        xP = xMaxClip;
      } else if (cP & 2) {
        xP += (yMinClip - yP) * dx / dy;
        yP = yMinClip;
      } else if (cP & 1) {
        xP += (yMaxClip - yP) * dx / dy;
        yP = yMaxClip;
      }
      cP = clipCode(xP, yP);
    } else {
      if (cQ & 8) {
        yQ += (xMinClip - xQ) * dy / dx;
        xQ = xMinClip;
      } else if (cQ & 4) {
        yQ += (xMaxClip - xQ) * dy / dx;
        xQ = xMaxClip;
      } else if (cQ & 2) {
        xQ += (yMinClip - yQ) * dx / dy;
        yQ = yMinClip;
      } else if (cQ & 1) {
        xQ += (yMaxClip - yQ) * dx / dy;
        yQ = yMaxClip;
      }
      cQ = clipCode(xQ, yQ);
    }
  }
  grsys_Move(xP, yP);
  grsys_Draw(xQ, yQ);
}

static int vpPlot_next = 0, vpPlot_size = 0;
vpNode *vpPlots = NULL;
void grsys_AppendPlot(float64 x, float64 y, int code) {
// Store point (x, y) and plotcode (0=up, 1=down) in queue.
  if (!iwCalled) {
      fprintf(stderr, "Call InitWindow before AppendPlot\n");
  }
  if (vpPlots == NULL) {
    vpPlot_size = 1;
    vpPlots = malloc(sizeof(vpNode));
  }
  if (vpPlot_next+1 >= vpPlot_size) {
    vpPlot_size = vpPlot_size * 2;
    vpPlots = realloc(vpPlots, vpPlot_size * sizeof(vpNode));
  }
  vpPlots[vpPlot_next++] = (vpNode){x, y, code};
  grsys_UpdateWindowBoundaries(x, y);
}

void grsys_GenPlot(void) {
  if (!grsys_inGrMode) {
    fprintf(stderr, "Call InitGr before GenPlot\n");
  }
  grsys_ViewportBoundaries(grsys_XMin, grsys_XMax, grsys_YMin, grsys_YMax, 0.9);
  for (int p = 0; p < vpPlot_next; p++) {
    float64 x = grsys_XViewport(vpPlots[p].x);
    float64 y = grsys_YViewport(vpPlots[p].y);
    if (vpPlots[p].code != 0) {
      grsys_Draw(x, y);
    } else {
      grsys_Move(x, y);
    }
  }
}
