/* grOGL3.c -
 *
 *     *********************************************************************
 *     * Copyright (C) 1985, 1990 Regents of the University of California. *
 *     * Permission to use, copy, modify, and distribute this              *
 *     * software and its documentation for any purpose and without        *
 *     * fee is hereby granted, provided that the above copyright          *
 *     * notice appear in all copies.  The University of California        *
 *     * makes no representations about the suitability of this            *
 *     * software for any purpose.  It is provided "as is" without         *
 *     * express or implied warranty.  Export of this software outside     *
 *     * of the United States of America may require an export license.    *
 *     *********************************************************************
 *
 * This file contains additional functions to manipulate an X window system
 * color display.  Included here are device-dependent routines to draw and
 * erase text and draw a grid.
 *
 */

#include <stdio.h>
#include <string.h>

#include <GL/gl.h>
#include <GL/glx.h>
#include <GL/glu.h>

#include "utils/magic.h"
#include "utils/geometry.h"
#include "graphics/graphics.h"
#include "windows/windows.h"
#include "graphics/graphicsInt.h"
#include "textio/textio.h"
#include "utils/signals.h"
#include "utils/utils.h"
#include "utils/hash.h"
#include "dbwind/dbwind.h"
#include "database/fonts.h"
#include "grOGLInt.h"

/* C99 compat */
#include "utils/malloc.h"

extern Display *grXdpy;

static XFontStruct *grXFonts[4];
#define grSmallFont     grXFonts[0]
#define grMediumFont    grXFonts[1]
#define grLargeFont     grXFonts[2]
#define grXLargeFont    grXFonts[3]
GLuint	grXBases[4];



/*---------------------------------------------------------
 * groglDrawGrid:
 *	groglDrawGrid adds a grid to the grid layer, using the current
 *	write mask and color.
 *
 * Results:
 *	TRUE is returned normally.  However, if the grid gets too small
 *	to be useful, then nothing is drawn and FALSE is returned.
 *
 * Side Effects:    None.
 *---------------------------------------------------------
 */

bool
groglDrawGrid(
    Rect *prect,			/* A rectangle that forms the template
			         * for the grid.  Note:  in order to maintain
			         * precision for the grid, the rectangle
			         * coordinates are specified in units of
			         * screen coordinates multiplied by SUBPIXEL.
			         */
    int outline,		/* the outline style */
    Rect *clip)			/* a clipping rectangle */
{
    int xsize, ysize;
    int x, y;
    int xstart, ystart;
    int snum, low, hi, shifted;

    xsize = prect->r_xtop - prect->r_xbot;
    ysize = prect->r_ytop - prect->r_ybot;
    if (!xsize || !ysize)
	return FALSE;
    if (GRID_TOO_SMALL(xsize, ysize))
	return FALSE;

    xstart = prect->r_xbot % xsize;
    while (xstart < clip->r_xbot << SUBPIXELBITS) xstart += xsize;
    ystart = prect->r_ybot % ysize;
    while (ystart < clip->r_ybot << SUBPIXELBITS) ystart += ysize;

    groglSetLineStyle(outline);

    glBegin(GL_LINES);

    snum = 0;
    low = clip->r_ybot;
    hi = clip->r_ytop;
    for (x = xstart; x < (clip->r_xtop + 1) << SUBPIXELBITS; x += xsize)
    {
	shifted = x >> SUBPIXELBITS;
	glVertex2i(shifted, low);
	glVertex2i(shifted, hi);
	snum++;
    }

    snum = 0;
    low = clip->r_xbot;
    hi = clip->r_xtop;
    for (y = ystart; y < (clip->r_ytop + 1) << SUBPIXELBITS; y += ysize)
    {
	shifted = y >> SUBPIXELBITS;
	glVertex2i(low, shifted);
	glVertex2i(hi, shifted);
	snum++;
    }

    glEnd();
    return TRUE;
}


/*---------------------------------------------------------
 * groglPreLoadFont
 *	This local routine loads the X fonts used by Magic.
 *	At this time, we need the font information to size
 *	the window (making room for the title at the top),
 *	but with no rendering context set (no window to
 *	draw to) we defer transferring the font bitmaps to
 *	OpenGL display lists until later.
 *
 * Results:	Success/Fail
 *
 * Side Effects:    None.
 *---------------------------------------------------------
 */

bool
groglPreLoadFont()
{
    XFontStruct *fontInfo;
    int i;
    char *s;
    char *unable = "Unable to load font";

    static char *fontnames[4] = {
      X_FONT_SMALL,
      X_FONT_MEDIUM,
      X_FONT_LARGE,
      X_FONT_XLARGE };
    static char *optionnames[4] = {
      "small",
      "medium",
      "large",
      "xlarge"};

    for (i = 0; i < 4; i++) {
    	 s = XGetDefault(grXdpy,"magic",optionnames[i]);
	 if (s) fontnames[i] = s;
         if ((fontInfo = XLoadQueryFont(grXdpy, fontnames[i])) == NULL) {
	      TxError("%s %s\n",unable,fontnames[i]);
              if ((grXFonts[i]= XLoadQueryFont(grXdpy,GR_DEFAULT_FONT))==NULL) {
	           TxError("%s %s\n",unable,GR_DEFAULT_FONT);
		   return FALSE;
	      }
         }
	 grXFonts[i] = fontInfo;
    }
    return TRUE;
}


/*---------------------------------------------------------
 * groglLoadFont
 *	This local routine transfers the X font bitmaps
 *	into OpenGL display lists for simple text
 *	rendering.
 *
 * Results:	Success/Fail.
 *
 * Side Effects:    None.
 *---------------------------------------------------------
 */

bool
groglLoadFont()
{
    XFontStruct *fontInfo;
    Font id;
    unsigned int first, last, i;

    for (i = 0; i < 4; i++) {
	fontInfo = grXFonts[i];
	id = fontInfo->fid;
	first = fontInfo->min_char_or_byte2;
	last = fontInfo->max_char_or_byte2;

	grXBases[i] = glGenLists(last+1);
	if (grXBases[i] == 0) {
	    TxError("Out of display lists!\n");
	    return FALSE;
	}
	glXUseXFont(id, first, last-first+1, grXBases[i]+first);
    }
    return TRUE;
}


/*---------------------------------------------------------
 * groglSetCharSize:
 *	This local routine sets the character size in the display,
 *	if necessary.
 *
 * Results:	None.
 *
 * Side Effects:    None.
 *---------------------------------------------------------
 */

void
groglSetCharSize(
    int size)		/* Width of characters */
{
    oglCurrent.fontSize = size;

    switch (size) {
    case GR_TEXT_DEFAULT:
    case GR_TEXT_SMALL:
	oglCurrent.font = grSmallFont;
	break;
    case GR_TEXT_MEDIUM:
	oglCurrent.font = grMediumFont;
	break;
    case GR_TEXT_LARGE:
	oglCurrent.font = grLargeFont;
	break;
    case GR_TEXT_XLARGE:
	oglCurrent.font = grXLargeFont;
	break;
    default:
	    TxError("%s%d\n", "groglSetCharSize: Unknown character size ",
		size );
	    break;
	break;
    }
}


/*
 * ----------------------------------------------------------------------------
 * GrOGLTextSize --
 *
 *	Determine the size of a text string.
 *
 * Results:
 *	Returns 0 when 'r' updated, otherwise -1 on error (no side-effects).
 *
 * Side effects:
 *	A rectangle is filled in that is the size of the text in pixels.
 *	The origin (0, 0) of this rectangle is located on the baseline
 *	at the far left side of the string.
 * ----------------------------------------------------------------------------
 */

int
GrOGLTextSize(
    char *text,
    int size,
    Rect *r)
{
    XCharStruct overall;
    XFontStruct *font;
    int dir,fa,fd;

    switch (size) {
    case GR_TEXT_DEFAULT:
    case GR_TEXT_SMALL:
	font = grSmallFont;
	break;
    case GR_TEXT_MEDIUM:
	font = grMediumFont;
	break;
    case GR_TEXT_LARGE:
	font = grLargeFont;
	break;
    case GR_TEXT_XLARGE:
	font = grXLargeFont;
	break;
    default:
        font = NULL;
	TxError("%s%d\n", "GrOGLTextSize: Unknown character size ",
		size );
	break;
    }
    if (font == NULL) return -1;
    XTextExtents(font, text, strlen(text), &dir, &fa, &fd, &overall);

    r->r_ytop = overall.ascent;
    r->r_ybot = -overall.descent;
    r->r_xtop = overall.width - overall.lbearing;
    r->r_xbot = -overall.lbearing - 1;
    return 0;
}


/*
 * ----------------------------------------------------------------------------
 * GrOGLReadPixel --
 *
 *	Read one pixel from the screen.
 *
 * Results:
 *	An integer containing the pixel's color.
 *	(Except OpenGL has no such function, so we just return 0)
 *
 * Side effects:
 *	none.
 *
 * ----------------------------------------------------------------------------
 */

int
GrOGLReadPixel(
    MagWindow *w,
    int x,
    int y)
{
   return 0;
}


/*
 * ----------------------------------------------------------------------------
 * GrOGLBitBlt --
 *
 *	Copy information from one part of the screen to the other.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	changes the screen.
 * ----------------------------------------------------------------------------
 */

void
GrOGLBitBlt(
    Rect *r,
    Point *p)
{
    glCopyPixels(r->r_xbot, r->r_ybot, r->r_xtop - r->r_xbot + 1,
		r->r_ytop - r->r_ybot + 1, GL_COLOR);
}

#ifdef VECTOR_FONTS

/*
 *----------------------------------------------------------------------
 *
 * Technically, there should be no self-intersecting polygons in outline
 * fonts.  However, decomposition of bezier curves into line segments
 * may occasionally produce one, so it needs to be handled.
 *----------------------------------------------------------------------
 */

void
myCombine(GLdouble coords[3], GLdouble *vertex_data[4],
          GLfloat weight[4], GLdouble **outData, void *dataptr)
{
    /* This needs to be free'd at the end of gluTessEndPolygon()! */
    GLdouble *new = (GLdouble *)mallocMagic(2 * sizeof(GLdouble));
    new[0] = coords[0];
    new[1] = coords[1];
    *outData = new;
    /* Diagnostic */
    TxError("Intersecting polygon in char \"%c\" at %g %g!\n",
	*((char *)dataptr), coords[0], coords[1]);
}

/*
 *----------------------------------------------------------------------
 * Draw a text character
 * This routine differs from grtoglFillPolygon() in that it uses the
 * glu library to handle non-convex polygons as may appear in font
 * outlines.
 *----------------------------------------------------------------------
 */

void
groglDrawCharacter(
    FontChar *clist,
    unsigned char tc,
    int pixsize)
{
    Point *tp;
    int np, nptotal;
    int i, j;
    static GLUtesselator *tess = NULL;
    static GLdouble *v = NULL;
    static int maxnp = 0;
    FontChar *ccur;

    if (pixsize < 5) return;	/* Label too small to be useful */

    if (tess == NULL)
    {
	tess = gluNewTess();
	gluTessCallback(tess, GLU_TESS_BEGIN, (_GLUfuncptr)glBegin);
	gluTessCallback(tess, GLU_TESS_VERTEX, (_GLUfuncptr)glVertex3dv);
	gluTessCallback(tess, GLU_TESS_END, (_GLUfuncptr)glEnd);
	gluTessCallback(tess, GLU_TESS_COMBINE_DATA, (_GLUfuncptr)myCombine);
    }
    // Boundary-only does not look particularly good. . .
    gluTessProperty(tess, GLU_TESS_BOUNDARY_ONLY, GL_FALSE);

    nptotal = 0;
    for (ccur = clist; ccur != NULL; ccur = ccur->fc_next)
	nptotal += ccur->fc_numpoints;

    if (nptotal > maxnp)
    {
	if (v != NULL) freeMagic((char *)v);
	maxnp = nptotal;
	v = (GLdouble *)mallocMagic(nptotal * 3 * sizeof(GLdouble));
    }

    j = 0;
    for (ccur = clist; ccur != NULL; ccur = ccur->fc_next)
    {
	tp = ccur->fc_points;
	np = ccur->fc_numpoints;

	for (i = 0; i < np; i++, j += 3) {
	    v[j] = tp[i].p_x;
	    v[j + 1] = tp[i].p_y;
	    v[j + 2] = 0;
	}
    }

    gluTessBeginPolygon(tess, (GLvoid *)(&tc));
    j = 0;
    for (ccur = clist; ccur != NULL; ccur = ccur->fc_next)
    {
	np = ccur->fc_numpoints;
	gluTessBeginContour(tess);
	for (i = 0; i < np; i++, j += 3) {
	    gluTessVertex(tess, &v[j], &v[j]);
	}
	gluTessEndContour(tess);
    }
    gluTessEndPolygon(tess);
}

/*---------------------------------------------------------
 * groglFontText:
 *
 *	This routine draws text from font vectors using the
 *	font vector routines in DBlabel.c.  Text is clipped
 *	to the clipping rectangle.
 *
 *	For speed, we should be transferring the font
 *	vectors into OpenGL display lists!
 *
 *---------------------------------------------------------
 */

void
groglFontText(
    char *text,			/* The text to be drawn */
    int   font,			/* Font to use from fontList */
    int   size,			/* Pixel size of the font */
    int	  rotate,		/* Text rotation */
    Point *pos,			/* Text base position */
    Rect  *clip,		/* Clipping area */
    LinkedRect *obscure)	/* List of obscuring areas */
{
    char *tptr;
    Point *coffset;		/* vector to next character */
    Rect *cbbox;
    GLfloat fsize, matvals[16];
    FontChar *clist;
    int cheight, baseline;
    float tmp;

    /* Keep it simple for now---ignore clip and obscure */

    glDisable(GL_BLEND);
    glPushMatrix();
    glTranslated(pos->p_x, pos->p_y, 0);
    glRotated(rotate, 0, 0, 1);

    /* Get label size */
    cbbox = &DBFontList[font]->mf_extents;

    fsize = (GLfloat)size / (GLfloat)cbbox->r_ytop;
    glScalef(fsize, fsize, 1.0);

    /* Adjust to baseline */
    baseline = 0;
    for (tptr = text; *tptr != '\0'; tptr++)
    {
	DBFontChar(font, *tptr, NULL, NULL, &cbbox);
	if (cbbox->r_ybot < baseline)
	    baseline = cbbox->r_ybot;
    }
    glTranslated(0, -baseline, 0);

    for (tptr = text; *tptr != '\0'; tptr++)
    {
	DBFontChar(font, *tptr, &clist, &coffset, NULL);
	groglDrawCharacter(clist, *tptr, size);
	glTranslated(coffset->p_x, coffset->p_y, 0);
    }
    glPopMatrix();
}

#endif /* VECTOR_FONTS */

static GC grXcopyGC = (GC)NULL;

/*
 * ----------------------------------------------------------------------------
 * groglFreeBackingStore --
 *	Free up Pixmap memory for a backing store cell.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	memory Free'd
 * ----------------------------------------------------------------------------
 */

void
groglFreeBackingStore(
    MagWindow *window)
{
    Pixmap pmap = (Pixmap)window->w_backingStore;
    if (pmap == (Pixmap)NULL) return;
    XFreePixmap(grXdpy, pmap);
    window->w_backingStore = (ClientData)NULL;
    /* XFreeGC(grXdpy, grXcopyGC); */
    /* TxPrintf("groglFreeBackingStore called\n"); */
}

/*
 * ----------------------------------------------------------------------------
 * groglCreateBackingStore --
 *	Create Pixmap memory for a backing store cell and copy data
 *	from the window into it.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	memory Allocated.
 *
 * ----------------------------------------------------------------------------
 */

void
groglCreateBackingStore(
    MagWindow *w)
{
    Pixmap pmap;
    Window wind = (Window)w->w_grdata;
    unsigned int width, height;
    GC gc;
    XGCValues gcValues;

    /* ignore for all windows except layout */
    if (w->w_client != DBWclientID) return;

    /* deferred */
    if (w->w_grdata == PTR2CD(NULL)) return;

    width = w->w_screenArea.r_xtop - w->w_screenArea.r_xbot;
    height = w->w_screenArea.r_ytop - w->w_screenArea.r_ybot;

    if (w->w_backingStore != (ClientData)NULL) groglFreeBackingStore(w);

    if (grXcopyGC == (GC)NULL)
    {
	gcValues.graphics_exposures = FALSE;
	grXcopyGC = XCreateGC(grXdpy, wind, GCGraphicsExposures, &gcValues);
    }

    pmap = XCreatePixmap(grXdpy, wind, width, height, oglCurrent.depth);
    w->w_backingStore = (ClientData)pmap;

    /* TxPrintf("groglCreateBackingStore area %d %d %d %d\n",
	w->w_screenArea.r_xbot, w->w_screenArea.r_ybot,
	w->w_screenArea.r_xtop, w->w_screenArea.r_ytop); */
}

/*
 * ----------------------------------------------------------------------------
 * groglGetBackingStore --
 *	Copy data from a backing store Pixmap into the indicated window.
 *
 * Results:
 *	TRUE if backing store was copied successfully, FALSE if not.
 *
 * Side effects:
 *	Data copied into Pixmap memory.
 *
 * ----------------------------------------------------------------------------
 */

bool
groglGetBackingStore(
    MagWindow *w,
    Rect *area)
{
    Pixmap pmap;
    Window wind = (Window)w->w_grdata;
    unsigned int width, height;
    int ybot;
    int xoff, yoff;
    Rect r;

    pmap = (Pixmap)w->w_backingStore;
    if (pmap == (Pixmap)NULL)
	return FALSE;

    /* Make a local copy of area so we don't disturb the original */
    r = *area;
    GeoClip(&r, &(w->w_screenArea));

    width = r.r_xtop - r.r_xbot;
    height = r.r_ytop - r.r_ybot;
    ybot = glTransY(w, r.r_ytop);

    xoff = w->w_screenArea.r_xbot - w->w_allArea.r_xbot;
    yoff = w->w_allArea.r_ytop - w->w_screenArea.r_ytop;

    XCopyArea(grXdpy, pmap, wind, grXcopyGC, r.r_xbot - xoff, ybot - yoff,
		width, height, r.r_xbot, ybot);

    /* TxPrintf("groglGetBackingStore %d %d %d %d\n",
		r.r_xbot, r.r_ybot, r.r_xtop, r.r_ytop); */
    return TRUE;
}

/*
 * ----------------------------------------------------------------------------
 * groglScrollBackingStore --
 *	Enable fast scrolling by shifting part of the backing store
 *	from one position to another, with the amount of shift indicated
 *	by the X and/or Y value of the indicated point.
 *
 * Results:
 *	TRUE on success, FALSE on failure.
 *
 * Side effects:
 *	Data shifted in Pixmap memory.
 *
 * ----------------------------------------------------------------------------
 */

bool
groglScrollBackingStore(
    MagWindow *w,
    Point *shift)
{
    Pixmap pmap;
    unsigned int width, height;
    int xorigin, yorigin, xshift, yshift;

    pmap = (Pixmap)w->w_backingStore;
    if (pmap == (Pixmap)NULL)
    {
	TxPrintf("groglScrollBackingStore %d %d failure\n",
		shift->p_x, shift->p_y);
	return FALSE;
    }

    width = w->w_screenArea.r_xtop - w->w_screenArea.r_xbot;
    height = w->w_screenArea.r_ytop - w->w_screenArea.r_ybot;
    xorigin = 0;
    yorigin = 0;
    xshift = shift->p_x;
    yshift = -shift->p_y;

    if (xshift > 0)
	width -= xshift;
    else if (xshift < 0)
    {
	width += xshift;
	xorigin = -xshift;
	xshift = 0;
    }
    if (yshift > 0)
	height -= yshift;
    else if (yshift < 0)
    {
	height += yshift;
	yorigin = -yshift;
	yshift = 0;
    }

    XCopyArea(grXdpy, pmap, pmap, grXcopyGC, xorigin, yorigin, width, height,
		xshift, yshift);

    /* TxPrintf("groglScrollBackingStore %d %d\n", shift->p_x, shift->p_y); */
    return TRUE;
}

/*
 * ----------------------------------------------------------------------------
 * groglPutBackingStore --
 *	Copy data from the window into backing store.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Graphics drawing into the window.
 * ----------------------------------------------------------------------------
 */

void
groglPutBackingStore(
    MagWindow *w,
    Rect *area)
{
    Pixmap pmap = (Pixmap)w->w_backingStore;
    Window wind = (Window)w->w_grdata;
    unsigned int width, height;
    int ybot, xoff, yoff;

    if (pmap == (Pixmap)NULL) return;

    /* Attempting to write backing store into an obscured	*/
    /* window (which we keep track of with Visibility events)	*/
    /* causes backing store to be invalid.			*/

    if (w->w_flags & WIND_OBSCURED)
    {
	groglFreeBackingStore(w);
	w->w_backingStore = (ClientData)NULL;
	return;
    }

    width = area->r_xtop - area->r_xbot;
    height = area->r_ytop - area->r_ybot;
    ybot = glTransY(w, area->r_ytop);

    xoff = w->w_screenArea.r_xbot - w->w_allArea.r_xbot;
    yoff = w->w_allArea.r_ytop - w->w_screenArea.r_ytop;

    XCopyArea(grXdpy, wind, pmap, grXcopyGC, area->r_xbot, ybot,
		width, height, area->r_xbot - xoff, ybot - yoff);

    /* TxPrintf("groglPutBackingStore %d %d %d %d\n",
		area->r_xbot, area->r_ybot, area->r_xtop, area->r_ytop); */
}


/*---------------------------------------------------------
 * groglPutText:
 *      (modified on SunPutText)
 *
 *	This routine puts a chunk of text on the screen in the current
 *	color, size, etc.  The caller must ensure that it fits on
 *	the screen -- no clipping is done except to the obscuring rectangle
 *	list and the clip rectangle.
 *
 * Results:
 *	none.
 *
 * Side Effects:
 *	The text is drawn on the screen.
 *
 *---------------------------------------------------------
 */

void
groglPutText(
    char *text,			/* The text to be drawn. */
    Point *pos,			/* A point located at the leftmost point of
				 * the baseline for this string.
				 */
    Rect *clip,			/* A rectangle to clip against */
    LinkedRect *obscure)	/* A list of obscuring rectangles */
{
    Rect location;
    Rect overlap;
    Rect textrect;
    LinkedRect *ob;
    void grOGLGeoSub();
    int i;
    float tscale;

    if (GrOGLTextSize(text, oglCurrent.fontSize, &textrect) < 0)
        return;

    location.r_xbot = pos->p_x + textrect.r_xbot;
    location.r_xtop = pos->p_x + textrect.r_xtop;
    location.r_ybot = pos->p_y + textrect.r_ybot;
    location.r_ytop = pos->p_y + textrect.r_ytop;

    /* erase parts of the bitmap that are obscured */
    for (ob = obscure; ob != NULL; ob = ob->r_next)
    {
	if (GEO_TOUCH(&ob->r_r, &location))
	{
	    overlap = location;
	    GeoClip(&overlap, &ob->r_r);
	    grOGLGeoSub(&location, &overlap);
	}
    }

    overlap = location;
    GeoClip(&overlap, clip);

    /* copy the text to the color screen */
    if ((overlap.r_xbot < overlap.r_xtop)&&(overlap.r_ybot <= overlap.r_ytop))
    {
	glScissor(overlap.r_xbot, overlap.r_ybot, overlap.r_xtop - overlap.r_xbot,
		overlap.r_ytop - overlap.r_ybot);
	glEnable(GL_SCISSOR_TEST);
	glDisable(GL_BLEND);
	glRasterPos2i(pos->p_x, pos->p_y);
	glListBase(grXBases[(oglCurrent.fontSize == GR_TEXT_DEFAULT) ?
		GR_TEXT_SMALL : oglCurrent.fontSize]);
	glCallLists(strlen(text), GL_UNSIGNED_BYTE, (unsigned char *)text);
	glDisable(GL_SCISSOR_TEST);
    }
}


/* grOGLGeoSub:
 *	return the tallest sub-rectangle of r not obscured by area
 *	area must be within r.
 */

void
grOGLGeoSub(
Rect *r,		/* Rectangle to be subtracted from. */
Rect *area)		/* Area to be subtracted. */
{
    if (r->r_xbot == area->r_xbot) r->r_xbot = area->r_xtop;
    else
    if (r->r_xtop == area->r_xtop) r->r_xtop = area->r_xbot;
    else
    if (r->r_ybot <= area->r_ybot) r->r_ybot = area->r_ytop;
    else
    if (r->r_ytop == area->r_ytop) r->r_ytop = area->r_ybot;
    else
    r->r_xtop = area->r_xbot;
}
