/* hgCustom - Custom track management CGI. */

/* Copyright (C) 2014 The Regents of the University of California 
 * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
#include "common.h"
#include "obscure.h"
#include "linefile.h"
#include "hash.h"
#include "cart.h"
#include "cheapcgi.h"
#include "web.h"
#include "htmshell.h"
#include "hdb.h"
#include "hui.h"
#include "hCommon.h"
#include "customTrack.h"
#include "customFactory.h"
#include "portable.h"
#include "errCatch.h"
#include "knetUdc.h"
#include "udc.h"
#include "net.h"
#include "jsHelper.h"
#include <signal.h>
#include "trackHub.h"
#include "botDelay.h"
#include "chromAlias.h"

static long loadTime = 0;
static boolean issueBotWarning = FALSE;
#define delayFraction   0.25	/* same as hgTracks */

void usage()
/* Explain usage and exit. */
{
errAbort(
  "hgCustom - Custom track management CGI\n"
  "usage:\n"
  "   hgCustom <CGI settings>\n"
  );
}

/* DON'T EDIT THIS -- use CGI param "&measureTiming=." */
static boolean measureTiming = FALSE;

#define TEXT_ENTRY_ROWS 7
#define TEXT_ENTRY_COLS 73
#define CONFIG_ENTRY_ROWS 3
#define SAVED_LINE_COUNT  50

/* CGI variables */
#define hgCt		 "hgct_"  /* prefix for all control variables; these are removed at end! */
#define hgCtNoRemove	 "hgctNoRemove_"
/* these are shared with other modules */
#define hgCtDataText      CT_CUSTOM_TEXT_ALT_VAR
#define hgCtDataFile      CT_CUSTOM_FILE_VAR
#define hgCtDataFileName  CT_CUSTOM_FILE_NAME_VAR
#define hgCtDocText       CT_CUSTOM_DOC_TEXT_VAR
#define hgCtDocFile       CT_CUSTOM_DOC_FILE_VAR
#define hgCtTable         CT_SELECTED_TABLE_VAR
#define hgCtUpdatedTable  CT_UPDATED_TABLE_VAR

/* misc */
#define hgCtUpdatedTrack "hgct_updatedTrack"
#define hgCtDeletePrefix "hgct_del"
#define hgCtRefreshPrefix "hgct_refresh"
#define hgCtConfigLines   "hgct_configLines"
#define hgCtNavDest 	 hgCtNoRemove "navDest"

/* commands */
#define hgCtDo		  hgCt   "do_"	  /* prefix for all commands */
#define hgCtDoAdd	  hgCtDo "add"
#define hgCtDoDelete	  hgCtDo "delete"
#define hgCtDoDeleteSet	  hgCtDo "delete_set"
#define hgCtDoDeleteClr	  hgCtDo "delete_clr"
#define hgCtDoRefresh     hgCtDo "refresh"
#define hgCtDoRefreshSet  hgCtDo "refresh_set"
#define hgCtDoRefreshClr  hgCtDo "refresh_clr"
#define hgCtDoGenomeBrowser	  hgCtDo "gb"
#define hgCtDoTableBrowser	  hgCtDo "tb"
#ifdef PROGRESS_METER
#define hgCtDoProgress	  hgCtDo "progress"
#endif

/* Global variables */
struct cart *cart;
struct hash *oldVars = NULL;
char *excludeVars[] = {"Submit", "submit", "SubmitFile", "ContinueWithWarn", NULL};
char *database = NULL;
char *organism = NULL;
struct customTrack *ctList = NULL;

void makeClearButton(char *field)
/* UI button that clears a text field */
{
char id[256];
safef(id, sizeof id, "%s_clear", field);
char javascript[1024];
safef(javascript, sizeof javascript,
        "document.mainForm.%s.value = '';", field);
cgiMakeOnClickButton(id, javascript, "&nbsp;Clear&nbsp;");
}

void addIntro()
/* display overview and help message for "add" screen */
{
puts(" Data must be formatted in\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigBed.html'>bigBed</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/barChart.html'>bigBarChart</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigChain.html'>bigChain</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigGenePred.html'>bigGenePred</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/interact.html'>bigInteract</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigLolly.html'>bigLolly</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigMaf.html'>bigMaf</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigPsl.html'>bigPsl</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bigWig.html'>bigWig</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bam.html'>BAM</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/barChart.html'>barChart</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/vcf.html'>VCF</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format1'>BED</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format1.7'>BED detail</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/bedgraph.html'>bedGraph</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format13'>broadPeak</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/cram.html'>CRAM</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format3'>GFF</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format4'>GTF</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format23'>hic</A>,\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/interact.html'>interact</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format5'>MAF</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format12'>narrowPeak</A>,\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format10'>Personal Genome SNP,</A>\n"
" <A TARGET=_BLANK HREF='../FAQ/FAQformat.html#format2'>PSL</A>,\n"
" or <A TARGET=_BLANK HREF='../goldenPath/help/wiggle.html'>WIG</A>\n"
" formats.<br>"
" <li>You can paste just the URL to the file, without a \"track\" line, for bigBed, bigWig, bigGenePred, BAM and VCF.<br>"
" <li>To configure the display, set\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#TRACK'>track</A>\n"
" and"
" <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#BROWSER'>browser</A>\n"
" line attributes as described in the \n"
" <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html'>User's Guide</A>.<br>\n"
" Examples are\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/customTrack.html#EXAMPLE1'>here</A>.\n"
" If you do not have web-accessible data storage available, please see the\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/hgTrackHubHelp.html#Hosting'>Hosting</A> section of the Track Hub Help documentation.\n<br><br>"
" Please note a much more efficient way to load data is to use\n"
" <A TARGET=_BLANK HREF='../goldenPath/help/hgTrackHubHelp.html'>Track Hubs</A>, which are loaded\n" 
" from the <A TARGET=_BLANK HREF='hgHubConnect'>Track Hubs Portal</A> found in the menu under My Data.\n"
);
}

void addCustomForm(struct customTrack *ct, char *err, boolean warnOnly)
/* display UI for adding custom tracks by URL or pasting data */
{
char *dataUrl = NULL;
char buf[1024];

boolean gotClade = FALSE;
boolean isUpdateForm = FALSE;
if (ct)
    {
    isUpdateForm = TRUE;
    dataUrl = ctDataUrl(ct);
    }
else
    /* add form needs clade for assembly menu */
    gotClade = hGotClade();

jsIncludeFile("jquery.js", NULL);
jsIncludeFile("hgCustom.js", NULL);
jsIncludeFile("utils.js", NULL);
jsIncludeFile("ajax.js", NULL);

/* main form */
printf("<FORM ACTION=\"%s\" METHOD=\"%s\" "
    " ENCTYPE=\"multipart/form-data\" NAME=\"mainForm\" id='mainForm'>\n",
    hgCustomName(), cartUsualString(cart, "formMethod", "POST"));
jsOnEventById("submit", "mainForm", "$('input[name=Submit]').attr('disabled', 'disabled');");
cartSaveSession(cart);

if (!isUpdateForm)
    {
    /* Print clade, genome and assembly  */
    /* NOTE: this uses an additional, hidden form (orgForm), below */
    char *onChangeDb = "document.orgForm.db.value = document.mainForm.db.options[document.mainForm.db.selectedIndex].value; document.orgForm.submit();";
    char *onChangeOrg = "document.orgForm.org.value = document.mainForm.org.options[document.mainForm.org.selectedIndex].value; document.orgForm.db.value = 0; document.orgForm.submit();";
    char *onChangeClade = "document.orgForm.clade.value = document.mainForm.clade.options[document.mainForm.clade.selectedIndex].value; document.orgForm.org.value = 0; document.orgForm.db.value = 0; document.orgForm.submit();";

    puts("<TABLE BORDER=0>\n");
    if (gotClade)
        {
        puts("<TR><TD>clade\n");
        printCladeListHtml(hOrganism(database), "change", onChangeClade);
        puts("&nbsp;&nbsp;&nbsp;");
        puts("genome\n");
        printGenomeListForCladeHtml(database, "change", onChangeOrg);
        }
    else
        {
        puts("<TR><TD>genome\n");
        printGenomeListHtml(database, "change", onChangeOrg);
        }
    puts("&nbsp;&nbsp;&nbsp;");
    puts("assembly\n");
    printAssemblyListHtml(database, "change", onChangeDb);
    char *description = hFreezeFromDb(database);
    if ((description != NULL) && ! stringIn(database, description))
	{
	puts("&nbsp;&nbsp;&nbsp;");
	printf("[%s]", trackHubSkipHubName(database));
	}
    puts("</TD></TR></TABLE>\n");
    }

/* intro text */
puts("<P>");
if (isUpdateForm)
    puts("Update your custom track configuration, data, and/or documentation.");
else
    puts("Display your own data as custom annotation tracks in the browser.");
addIntro();
puts("<P>");

/* row for error message */
if (isNotEmpty(err))
    {
    char *fullErrString = replaceChars(err, "\n", "<br>\n");
    printf("<P><B>&nbsp;&nbsp;&nbsp;&nbsp;<span style='color:%s; font-style:italic;'>"
           "%s</span><P>%s</B><P>", warnOnly ? "ORANGE" : "RED", warnOnly ? "Warning" : "Error", fullErrString);
    freeMem(fullErrString);
    /* send two lines of the message to the apache error log also: */
    char *tmpString = replaceChars(err, "\n", " ");
    fprintf(stderr, "hgCustom load error: %s\n", tmpString);
    freeMem(tmpString);
    }

cgiSimpleTableStart();

/* first rows for update form are for track and browser line entry */
if (isUpdateForm)
    {
    /* row for instructions */
    cgiSimpleTableRowStart();
    cgiSimpleTableFieldStart();
    if (dataUrl)
        puts("Configuration:");
    else
        {
        puts("Edit configuration:");
        }
    cgiTableFieldEnd();
    cgiTableField("&nbsp;");
    puts("<TD ALIGN='RIGHT'>");
    if (warnOnly)
	{
	cgiMakeButtonWithOnClick("ContinueWithWarn", "Continue with Warning", NULL, "return submitClick(this);");
	printf("&nbsp;");
	jsInline(
	    "$('textarea').change(function() {\n"
	    "    $('#ContinueWithWarn').hide();\n"
	    "});\n");
	}
    cgiMakeButtonWithOnClick("Submit", "Submit", NULL, "return submitClick(this);");
    printf("<img id='loadingImg' src='../images/loading.gif' />\n");
    cgiTableFieldEnd();
    cgiTableField("&nbsp;");
    cgiTableRowEnd();

    /* row for text entry box */
    cgiSimpleTableRowStart();
    puts("<TD COLSPAN=2>");
    if (dataUrl)
        {
        /* can't update via pasting if loaded from URL */
        cgiMakeTextAreaDisableable(hgCtConfigLines,
            cartUsualString(cart, hgCtConfigLines, customTrackUserConfig(ct)),
                            CONFIG_ENTRY_ROWS, TEXT_ENTRY_COLS, TRUE);
        }
    else
        {
        cgiMakeTextArea(hgCtConfigLines,
                cartUsualString(cart, hgCtConfigLines, customTrackUserConfig(ct)),
                            CONFIG_ENTRY_ROWS, TEXT_ENTRY_COLS);
        }
    cgiTableFieldEnd();

    cgiSimpleTableFieldStart();
    cgiSimpleTableStart();

    cgiSimpleTableRowStart();
    cgiSimpleTableFieldStart();
    cgiTableFieldEnd();
    cgiTableRowEnd();

    cgiTableEnd();
    cgiTableFieldEnd();
    cgiTableRowEnd();
    }

/* next row - label entry for file upload */
cgiSimpleTableRowStart();
if (isUpdateForm)
    {
    /* update existing */
    /* extra space */
    cgiSimpleTableRowStart();
    puts("<TD STYLE='padding-top:9';\"></TD>");
    cgiTableRowEnd();
    if (dataUrl)
        cgiTableField("Data:");
    else
        cgiTableField("Paste in replacement data:");
    }
else
    cgiTableField("Paste URLs or data:");

if (isUpdateForm && dataUrl)
    cgiTableField("&nbsp");
else
    {
    puts("<TD ALIGN='RIGHT'>");
    puts("Or upload: ");
    cgiMakeFileEntry(hgCtDataFile);
    cgiTableFieldEnd();
    jsInline(
            "$(\"[name='hgt.customFile']\").change(function(ev) { \n"
            "   var fname = ev.target.files[0].name; \n"
            "   var ext = fname.split('.').pop().toLowerCase(); \n"
            "   var warnExts = ['bigbed', 'bb', 'bam', 'bigwig', 'bw', 'jpeg', 'pdf', 'jpg', 'png', 'hic', 'cram'];\n"
            "   if (warnExts.indexOf(ext) >= 0) {\n"
            "       alert('You are trying to upload a binary file on this page, but the Genome Browser server needs access to binary files via the internet.'+"
            "          ' Therefore, you will need to store the files on a web server, then paste the URLs to them on this page, or upload a text file with \"track\" lines '+"
            "          ' and configuration settings that point to the file URLs. Please read the documentation'+"
            "          ' referenced at the top of this page or contact us for more information.');\n"
            "       $(\"[name='hgt.customFile']\")[0].value = '';"
            "   }\n"
            "});\n"
            );
    }

if (!isUpdateForm)
    {
    cgiSimpleTableFieldStart();
    if (warnOnly)
	{
	cgiMakeButtonWithOnClick("ContinueWithWarn", "Continue with Warning", NULL, "return submitClick(this);");
	printf("&nbsp;");
	jsInline(
	    "$('textarea').change(function() {\n"
	    "    $('#ContinueWithWarn').hide();\n"
	    "});\n");
	}
    cgiMakeButtonWithOnClick("Submit", "Submit", NULL, "return submitClick(this);");
    printf("<img id='loadingImg' src='../images/loading.gif' />\n");
    cgiTableFieldEnd();
    }
cgiTableRowEnd();

/* next row - text entry box for  data, and clear button */
cgiSimpleTableRowStart();
puts("<TD COLSPAN=2>");
if (dataUrl)
    {
    /* can't update via pasting if loaded from URL */
    safef(buf, sizeof buf, "Replace data at URL: %s", ctDataUrl(ct));
    cgiMakeTextAreaDisableable(hgCtDataText, buf,
                                TEXT_ENTRY_ROWS, TEXT_ENTRY_COLS, TRUE);
    }
else
    {
    int rows = (isUpdateForm ? TEXT_ENTRY_ROWS - CONFIG_ENTRY_ROWS :
                                TEXT_ENTRY_ROWS);
    cgiMakeTextArea(hgCtDataText, cartUsualString(cart, hgCtDataText, ""),
                                    rows, TEXT_ENTRY_COLS);
    }

cgiTableFieldEnd();

cgiSimpleTableFieldStart();
cgiSimpleTableStart();

cgiSimpleTableRowStart();
cgiSimpleTableFieldStart();
if (!(isUpdateForm && dataUrl))
    {
    printf("<span id='loadingMsg'></span>\n");
    makeClearButton(hgCtDataText);
    }
cgiTableFieldEnd();
cgiTableRowEnd();

cgiTableEnd();
cgiTableFieldEnd();
cgiTableRowEnd();

/* extra space */
cgiSimpleTableRowStart();
cgiSimpleTableFieldStart();
cgiDown(0.7);
cgiTableFieldEnd();
cgiTableRowEnd();

/* next row - label for description text entry */
cgiSimpleTableRowStart();
cgiTableField("Optional track documentation: ");
if (isUpdateForm && ctHtmlUrl(ct))
    cgiTableField("&nbsp;");
else
    {
    puts("<TD ALIGN='RIGHT'>");
    puts("Or upload: ");
    cgiMakeFileEntry(hgCtDocFile);
    cgiTableFieldEnd();
    }
cgiTableRowEnd();

/* next row - text entry for description, and clear button(s) */
cgiSimpleTableRowStart();
puts("<TD COLSPAN=2>");

if (ct && ctHtmlUrl(ct))
    {
    safef(buf, sizeof buf, "Replace doc at URL: %s", dataUrl);
    cgiMakeTextAreaDisableable(hgCtDocText, buf,
                                    TEXT_ENTRY_ROWS, TEXT_ENTRY_COLS, TRUE);
    }
else
    {
    cgiMakeTextArea(hgCtDocText, cartUsualString(cart, hgCtDocText, ""),
                                    TEXT_ENTRY_ROWS, TEXT_ENTRY_COLS);
    cgiSimpleTableFieldStart();
    cgiSimpleTableStart();
    cgiSimpleTableRowStart();
    cgiSimpleTableFieldStart();
    makeClearButton(hgCtDocText);
    cgiTableFieldEnd();
    cgiTableRowEnd();
    cgiTableEnd();
    }
cgiTableFieldEnd();

cgiTableRowEnd();
cgiTableEnd();

/* help text at bottom of screen - link for HTML description template */
puts("Click <A HREF=\"../goldenPath/help/ct_description.txt\" TARGET=_blank>here</A> for an HTML document template that may be used for Genome Browser track descriptions.");

if (isUpdateForm)
    {
    /* hidden variables to identify track */
    cgiMakeHiddenVar(hgCtUpdatedTable, ct->tdb->track);
    char buf[512];
    char *shortLabel = htmlEncode(ct->tdb->shortLabel);
    char *longLabel = htmlEncode(ct->tdb->longLabel);
    safef(buf, sizeof buf, "track name='%s' description='%s'",
				shortLabel, longLabel);
    char *trackLine = htmlEncode(ctOrigTrackLine(ct));
    cgiMakeHiddenVar(hgCtUpdatedTrack, trackLine ? trackLine : buf);
    freeMem(trackLine);
    freeMem(shortLabel);
    freeMem(longLabel);
    }
else
    {
    /* hidden form to handle clade/genome/assembly dropdown.
     * This is at end of page for layout reasons (preserve vertical space) */
    puts("</FORM>");
    printf("<FORM STYLE=\"margin-bottom:0;\" ACTION=\"%s\" METHOD=\"GET\" NAME=\"orgForm\">", hgCustomName());
    cartSaveSession(cart);
    if (gotClade)
        printf("<INPUT TYPE=\"HIDDEN\" NAME=\"clade\" VALUE=\"\">\n");
    printf("<INPUT TYPE=\"HIDDEN\" NAME=\"org\" VALUE=\"%s\">\n", organism);
    printf("<INPUT TYPE=\"HIDDEN\" NAME=\"db\" VALUE=\"%s\">\n", database);
    printf("<INPUT TYPE=\"HIDDEN\" NAME=\"hgct_do_add\" VALUE=\"1\">\n");
    }
puts("</FORM>");
cgiDown(0.9);
}

void tableHeaderFieldStart(int columns)
{
/* print table column header with white text on black background */
printf("<TD COLSPAN=%d ALIGN='CENTER' BGCOLOR='#536ED3'>", columns);
}

void tableHeaderField(char *label, char *description)
{
/* print table column header with white text on black background */
puts("<TD ALIGN='CENTER' BGCOLOR='#536ED3' ");
if (description)
    printf("TITLE='%s'", description);
printf("><B>%s</B></TD> ", wrapWhiteFont(label));
}

void showCustomTrackList(struct customTrack *ctList, int numCts)
/* print table of custom tracks with controls */
{
struct customTrack *ct;
char buf[256];
char *pos = NULL;
char *dataUrl;
int colSpan = 4;

/* handle 'set all' and 'clr all' (won't be used if user has javascript enabled). */
boolean setAllDelete = FALSE;
boolean setAllUpdate = FALSE;
if (cartVarExists(cart, hgCtDoDeleteSet))
    setAllDelete = TRUE;
if (cartVarExists(cart, hgCtDoRefreshSet))
    setAllUpdate = TRUE;

/* determine which columns to display (avoid empty columns) */
int updateCt = 0, itemCt = 0, posCt = 0, errCt = 0;
for (ct = ctList; ct != NULL; ct = ct->next)
    {
    if (ctDataUrl(ct))
        updateCt++;
    if (ctItemCount(ct) > 0)
        itemCt++;
    if (ctInitialPosition(ct) || ctFirstItemPos(ct))
        posCt++;
    if (ct->networkErrMsg)
	errCt++;
    }
hTableStart();
cgiSimpleTableRowStart();
tableHeaderField("Name", "Short track identifier");
tableHeaderField("Description", "Long track identifier");
tableHeaderField("Type", "Data format of track");
tableHeaderField("Doc", "HTML track description");
if (itemCt)
    {
    tableHeaderField("Items", "Count of discrete items in track");
    colSpan++;
    }
if (posCt)
    {
    tableHeaderField("Pos"," Go to genome browser at default track position or first item");
    colSpan++;
    }
if (errCt)
    {
    tableHeaderField("Error"," Error in custom track");
    colSpan++;
    }

boolean showAllButtons = FALSE;
if (numCts > 3)
    showAllButtons = TRUE;

tableHeaderFieldStart(showAllButtons ? 2 : 1);
cgiMakeButtonWithMsg(hgCtDoDelete, "delete", "Remove custom track");
cgiTableFieldEnd();

/* add column with Update button if any custom tracks are updateable */
if (updateCt)
    {
    tableHeaderFieldStart(showAllButtons ? 2 : 1);
    cgiMakeButtonWithMsg(hgCtDoRefresh, "update", "Refresh from data URL");
    cgiTableFieldEnd();
    }

cgiTableRowEnd();
int butCount=0;
for (ct = ctList; ct != NULL; ct = ct->next)
    {
    /* Name  field */
    char *shortLabel = htmlEncode(ct->tdb->shortLabel);
    if ((ctDataUrl(ct) && ctHtmlUrl(ct)) ||
            sameString(ct->tdb->type, "chromGraph"))
        printf("<TR><TD>%s</A></TD>", shortLabel);
    else
	{
	char *cgiName = cgiEncode(ct->tdb->track);
        printf("<TR><TD><A TITLE='Update custom track: %s' HREF='%s?%s&%s=%s'>%s</A></TD>",
            shortLabel, hgCustomName(),cartSidUrlString(cart),
	    hgCtTable, cgiName, shortLabel);
	freeMem(cgiName);
	}
    freeMem(shortLabel);
    /* Description field */
    char *longLabel = htmlEncode(ct->tdb->longLabel);
    printf("<TD>%s</TD>", longLabel);
    freeMem(longLabel);
    /* Type field */
    printf("<TD>%s</TD>", ctInputType(ct));
    /* Doc field */
    printf("<TD ALIGN='CENTER'>%s</TD>",
                isNotEmpty(ct->tdb->html) ? "Y" : "&nbsp;");
    /* Items field */
    if (itemCt)
        {
        int count = ctItemCount(ct);
        if (count > 0)
            printf("<TD ALIGN='CENTER'>%d</TD>", count);
        else
            puts("<TD>&nbsp;</TD>");
        }
    /* Pos field; indicates initial position for the track,
     * or first element */
    if (posCt)
        {
        pos = ctInitialPosition(ct);
        if (!pos)
            pos = ctFirstItemPos(ct);
        if (pos)
            {
            char *chrom = cloneString(pos);
            chopSuffixAt(chrom, ':');
            if (hgOfficialChromName(database, chrom))
                printf("<TD><A HREF='%s?%s&position=%s&hgTracksConfigPage=notSet' TITLE=%s>%s:</A></TD>",
                    hgTracksName(), cartSidUrlString(cart),pos, pos, chrom);
            else
                puts("<TD>&nbsp;</TD>");
            }
        else
            puts("<TD>&nbsp;</TD>");
        }
    if (errCt)
	{
	if (ct->networkErrMsg)
	    {
	    char id[256];
	    safef(id, sizeof id, "_%d", butCount);
	    printf("\n<TD><A href='#' id='%s'>Show</A></TD>\n", id);
	    jsOnEventByIdF("click", id, "alert('%s');return false;",
		javaScriptLiteralEncode(ct->networkErrMsg));
	    }
	else
	    puts("<TD>&nbsp;</TD>");
	}
    /* Delete checkboxes */
    printf("<TD COLSPAN=%d ALIGN=CENTER>", showAllButtons ? 2 : 1);
    safef(buf, sizeof(buf), "%s_%s", hgCtDeletePrefix,
            ct->tdb->track);
    cgiMakeCheckBoxMore(buf, setAllDelete, "class='deleteCheckbox'");
    puts("</TD>");

    /* Update checkboxes */
    if (updateCt)
        {
        printf("<TD COLSPAN=%d ALIGN=CENTER>", showAllButtons ? 2 : 1);
        safef(buf, sizeof(buf), "%s_%s", hgCtRefreshPrefix,
                ct->tdb->track);
        if ((dataUrl = ctDataUrl(ct)) != NULL)
            {
            char more[2048];
            safef(more, sizeof(more), "class='updateCheckbox' title='refresh data from: %s'", dataUrl);
            cgiMakeCheckBoxMore(buf, setAllUpdate, more);
            }
        else
            puts("&nbsp;");
	puts("</TD>");
        }
    puts("</TR>\n");
    }
if (showAllButtons)
    {
    cgiSimpleTableRowStart();
    printf("<TD COLSPAN=%d ALIGN='RIGHT'>check all / clear all&nbsp;</TD>", colSpan);
    cgiSimpleTableFieldStart();
    cgiMakeButtonWithOnClick(hgCtDoDeleteSet, "+", "Select all for deletion", "$('.deleteCheckbox').attr('checked', true); return false;");
    cgiTableFieldEnd();
    cgiSimpleTableFieldStart();
    cgiMakeButtonWithOnClick(hgCtDoDeleteClr, "-", "Clear all for deletion", "$('.deleteCheckbox').attr('checked', false); return false;");
    cgiTableFieldEnd();
    if (updateCt)
        {
        cgiSimpleTableFieldStart();
        cgiMakeButtonWithOnClick(hgCtDoRefreshSet, "+", "Select all for update", "$('.updateCheckbox').attr('checked', true); return false;");
        cgiTableFieldEnd();
        cgiSimpleTableFieldStart();
        cgiMakeButtonWithOnClick(hgCtDoRefreshClr, "-", "Clear all for update", "$('.updateCheckbox').attr('checked', false); return false;");
        cgiTableFieldEnd();
        }
    cgiTableRowEnd();
    }
hTableEnd();
}

struct dbDb *getCustomTrackDatabases()
/* Get list of databases having custom tracks for this user.
 * Dispose of this with dbDbFreeList. */
{
struct dbDb *dbList = NULL, *dbDb;
char *db;

/* Get list of assemblies with custom tracks */
struct hashEl *hels = cartFindPrefix(cart, CT_FILE_VAR_PREFIX);
struct hashEl *hel = NULL;
for (hel = hels; hel != NULL; hel = hel->next)
    {
    /* TODO: chop actual prefix */
    db = chopPrefixAt(cloneString(hel->name), '_');
        /* TODO: check if file exists, if not remove ctfile_ var */
    dbDb = hDbDb(db);
    if (dbDb)
        slAddTail(&dbList, dbDb);
    }
return dbList;
}

static struct slPair *makeOtherCgiValsAndLabels()
/* Return {value, label} pairs with other CGIs the user might wish to jump to. */
{
struct slPair *valsAndLabels = slPairNew(hgTracksName(), "Genome Browser");
slAddHead(&valsAndLabels, slPairNew(hgTablesName(), "Table Browser"));
slAddHead(&valsAndLabels, slPairNew(hgVaiName(), "Variant Annotation Integrator"));
slAddHead(&valsAndLabels, slPairNew(hgIntegratorName(), "Data Integrator"));
slReverse(&valsAndLabels);
return valsAndLabels;
}

static void makeOtherCgiForm(char *pos)
/* Make a form for navigating to other CGIs. */
{
struct slPair *valsAndLabels = makeOtherCgiValsAndLabels();
// Default to the first CGI in the menu.
char *defaultCgi = valsAndLabels->name;
char *selected = cartUsualString(cart, hgCtNavDest, defaultCgi);
printf("<FORM STYLE=\"margin-bottom:0;\" METHOD=\"GET\" NAME=\"navForm\" ID=\"navForm\""
       " ACTION=\"%s\">\n", selected);
cartSaveSession(cart);
if (pos)
    cgiMakeHiddenVar("position", pos);
printf("view in ");
// Construct a menu of destination CGIs
puts(cgiMakeSingleSelectDropList(hgCtNavDest, valsAndLabels, selected, NULL, NULL,
 "change", "var newVal = $('#navSelect').val(); $('#navForm').attr('action', newVal);", NULL, "navSelect"));
cgiMakeButton("submit", "go");
puts("</FORM>");
}

static void manageCustomForm(char *warnMsg)
/* list custom tracks and display checkboxes so user can select for delete */
{

struct dbDb *dbList = getCustomTrackDatabases();
struct dbDb *dbDb = NULL;
/* add this database to the list, as it may have no custom
 * tracks, but we still want to see it in the menu */
slAddTail(&dbList, hDbDb(database));
slReverse(&dbList);
/* remove duplicate entry for this database, if any */
for (dbDb = dbList->next; dbDb != NULL; dbDb = dbDb->next)
    if (sameString(dbDb->name, database))
        slRemoveEl(&dbList, dbDb);

boolean assemblyMenu = FALSE;
if (slCount(dbList) > 1)
    assemblyMenu = TRUE;

if (assemblyMenu)
    {
    /* hidden form to handle genome/assembly dropdown */
    printf("<FORM STYLE=\"margin-bottom:0;\" ACTION=\"%s\" METHOD=\"GET\" NAME=\"orgForm\">", hgCustomName());
    cartSaveSession(cart);
    printf("<INPUT TYPE=\"HIDDEN\" NAME=\"org\" VALUE=\"%s\">\n", organism);
    printf("<INPUT TYPE=\"HIDDEN\" NAME=\"db\" VALUE=\"%s\">\n", database);
    puts("</FORM>");
    }

/* the main form contains a table of all tracks, with checkboxes to delete */
printf("<FORM ACTION=\"%s\" METHOD=\"%s\" NAME=\"mainForm\">\n",
           hgCustomName(), cartUsualString(cart, "formMethod", "POST"));
cartSaveSession(cart);

if (assemblyMenu)
    {
    /* Print clade, genome and assembly  */
    char *onChangeDb = "document.orgForm.db.value = document.mainForm.db.options[document.mainForm.db.selectedIndex].value; document.orgForm.submit();";
    char *onChangeOrg = "document.orgForm.org.value = document.mainForm.org.options[document.mainForm.org.selectedIndex].value; document.orgForm.db.value = 0; document.orgForm.submit();";

    puts("<TABLE BORDER=0>\n");
    puts("<TR><TD>genome\n");
    printSomeGenomeListHtml(database, dbList, "change", onChangeOrg);
    puts("&nbsp;&nbsp;&nbsp;");
    puts("assembly\n");
    printSomeAssemblyListHtml(database, dbList, "change", onChangeDb);
    puts("&nbsp;&nbsp;&nbsp;");
    printf("[%s]", database);
    puts("</TD></TR></TABLE><P>\n");
    }
else
    {
    char *assemblyName = hFreezeDateOpt(database);
    if (assemblyName == NULL)
	assemblyName = "default";

    printf("<B>genome:</B> %s &nbsp;&nbsp;&nbsp;<B>assembly:</B> %s &nbsp;&nbsp;&nbsp;[%s]\n",
            organism, assemblyName, database);
	}

if (measureTiming && (loadTime > 0))
    printf("\n<BR>load time: %ld ms<BR>\n", loadTime);
/* place for warning messages to appear */
if (isNotEmpty(warnMsg))
    {
    char *encoded = htmlEncode(warnMsg);
    printf("<P><B>&nbsp;&nbsp;&nbsp;&nbsp;%s", encoded);
    freeMem(encoded);
    }

/* count up number of custom tracks for this genome */
int numCts = slCount(ctList);

cgiSimpleTableStart();
cgiSimpleTableRowStart();
if (numCts)
    {
    puts("<TD VALIGN=\"TOP\">");
    showCustomTrackList(ctList, numCts);
    }
else
    puts("<TD VALIGN=\"TOP\"><B><EM>No custom tracks for this genome:<B></EM>&nbsp;&nbsp;");
puts("</TD>");

/* end mainForm; navigation menu has its own form. */

puts("</FORM>");

puts("<TD VALIGN=\"TOP\">");
puts("<TABLE BORDER=0>");

/* determine if there's a navigation position for this screen */
char *pos = NULL;
if (ctList)
    {
    pos = ctInitialPosition(ctList);
    if (!pos)
        pos = ctFirstItemPos(ctList);
    }

puts("<TR><TD>");
makeOtherCgiForm(pos);
puts("</TD></TR>");

/* button to add custom tracks */
puts("<TR><TD>");
printf("<INPUT TYPE=SUBMIT NAME=\"addTracksButton\" ID=\"addTracksButton\" VALUE=\"%s\" "
       "STYLE=\"margin-top: 5px\" >\n",
       "add custom tracks");
// This submits mainForm with a hidden input that tells hgCustom to show add tracks page:
jsOnEventByIdF("click", "addTracksButton", 
	"var $form = $(\"form[name='mainForm']\"); "
	"$form.append(\"<input name='%s' type='hidden'>\"); "
	"$form.submit();"
	, hgCtDoAdd);

puts("</TD></TR>");

puts("</TABLE>");
puts("</TD>");

cgiTableRowEnd();
cgiTableEnd();


// This vertically aligns the 'add tracks' button with the other-CGI select
jsInline(
    "function fitUnder($el1, $el2) { "
    "  var off1 = $el1.offset(); "
    "  var off2 = $el2.offset(); "
    "  off2.left = off1.left; "
    "  $el2.offset(off2);"
    "  $el2.width($el1.width()); "
    "};"
    "$(document).ready(function () { fitUnder($('#navSelect'), $('#addTracksButton')); });\n"
);

cartSetString(cart, "hgta_group", "user");
}

void helpCustom()
/* display documentation */
{
webNewSection("Loading Custom Tracks");
char *browserVersion;
if (btIE == cgiClientBrowser(&browserVersion, NULL, NULL) && *browserVersion < '8')
    puts("<span>");
else
    puts("<span style='position:relative; top:-1em;'>");
webIncludeHelpFile("customTrackLoad", FALSE);

puts("</span>");
}

void doBrowserLines(struct slName *browserLines, char **retErr)
/*  parse variables from browser lines into the cart */
{
char *err = NULL;
struct slName *bl;
for (bl = browserLines; bl != NULL; bl = bl->next)
    {
    char *words[96];
    int wordCount;

    wordCount = chopLine(bl->name, words);
    if (wordCount > 1)
        {
	char *command = words[1];
	if (sameString(command, "hide")
            || sameString(command, "dense")
            || sameString(command, "pack")
            || sameString(command, "squish")
            || sameString(command, "full"))
	    {
	    if (wordCount > 2)
	        {
		int i;
		for (i=2; i<wordCount; ++i)
		    {
		    char *s = words[i];
		    if (sameWord(s, "all"))
                        {
                        cartSetString(cart, "hgt.visAllFromCt", command);
                        }
                    else
                        {
                        char buf[256];
                        safef(buf, sizeof buf, "hgtct.%s", s);
                        cartSetString(cart, buf, command);
                        }
		    }
		}
	    }
	else if (sameString(command, "position"))
	    {
            char *chrom = NULL;
            int start = 0, end = 0;
	    if (wordCount < 3)
                {
	        err = "Expecting 3 words in browser position line";
                break;
                }
	    if (!hgParseChromRange(database, words[2], &chrom, &start, &end) ||
                start < 0 || end > hChromSize(database, chrom))
                {
	        err ="Invalid browser position (use chrN:123-456 format)";
                break;
                }
            cartSetString(cart, "position", words[2]);
	    }
	}
    }
if (retErr)
    *retErr = err;
}

#ifdef PROGRESS_METER
static void progressMeter()
{
printf("<FORM STYLE=\"margin-bottom:0;\" ACTION=\"%s\" METHOD=\"GET\" NAME=\"orgForm\">", hgCustomName());
cartSaveSession(cart);
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"org\" VALUE=\"%s\">\n", organism);
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"db\" VALUE=\"%s\">\n", database);
printf("<INPUT TYPE=\"HIDDEN\" NAME=\"hgct_do_add\" VALUE=\"1\">\n");
puts("</FORM>");
}
static void doProgress(char *err)
/* display progress meter to show loading process */
{
cartWebStart(cart, database, "Custom Track loading progress meter");
progressMeter();
// addCustomForm(NULL, err);
helpCustom();
cartWebEnd(cart);
}
#endif

static void webBotWarning()
/* display the overuse warning message in the javaScript warning text box
 * html output has already started at this point, just need to add the
 * warning handler and setup the warning javaScript box, warnings after
 * this will go to that text box
 */
{
pushWarnHandler(webVaWarn);
htmlWarnBoxSetup(stdout);
char *ip = getenv("REMOTE_ADDR");
botDelayMessage(ip, botDelayMillis);
}

void doAddCustom(char *err, boolean warnOnly)
/* display form for adding custom tracks.
 * Include error message, if any */
{
cartWebStart(cart, database, "Add Custom Tracks");
addCustomForm(NULL, err, warnOnly);
helpCustom();
if (issueBotWarning)
    webBotWarning();
cartWebEnd(cart);
}

void doUpdateCustom(struct customTrack *ct, char *err, boolean warnOnly)
/* display form for adding custom tracks.
 * Include error message, if any */
{
char *longLabel = htmlEncode(ct->tdb->longLabel);
cartWebStart(cart, database, "Update Custom Track: %s [%s]",
        longLabel, database);
freeMem(longLabel);
cartSetString(cart, hgCtDocText, ct->tdb->html);
addCustomForm(ct, err, warnOnly);
helpCustom();
cartWebEnd(cart);
}

static void doManageCustom(char *warnMsg)
/* display form for deleting & updating custom tracks.
 * Include warning message, if any */
{
cartWebStart(cart, database, "Manage Custom Tracks");
jsIncludeFile("jquery.js", NULL);
manageCustomForm(warnMsg);
webNewSection("Managing Custom Tracks");
webIncludeHelpFile("customTrackManage", FALSE);
if (issueBotWarning)
    webBotWarning();
cartWebEnd(cart);
}

char *fixNewData(struct cart *cart)
/* append a newline to incoming data, to keep custom preprocessor happy */
{
char *customText = cartUsualString(cart, hgCtDataText, "");
if (isNotEmpty(customText))
    {
    struct dyString *ds = dyStringNew(0);
    dyStringPrintf(ds, "%s\n", customText);
    customText = dyStringCannibalize(&ds);
    cartSetString(cart, hgCtDataText, customText);
    }
return customText;
}

char *replacedTracksMsg(struct customTrack *replacedCts)
/* make warning message listing replaced tracks */
{
struct customTrack *ct;

if (!slCount(replacedCts))
    return NULL;
struct dyString *dsWarn = dyStringNew(0);
dyStringAppend(dsWarn, "Replaced: ");
for (ct = replacedCts; ct != NULL; ct = ct->next)
    {
    if (ct != replacedCts)
	/* not the first */
	dyStringAppend(dsWarn, ", ");
    dyStringAppend(dsWarn, ct->tdb->shortLabel);
    }
return dyStringCannibalize(&dsWarn);
}

void doDeleteCustom()
/* remove custom tracks from list based on cart variables */
{
struct customTrack *ct;
for (ct = ctList; ct != NULL; ct = ct->next)
    {
    char var[256];
    safef(var, sizeof var, "%s_%s", hgCtDeletePrefix, ct->tdb->track);
    if (cartBoolean(cart, var))
	slRemoveEl(&ctList, ct);
    }
}

void doRefreshCustom(char **warnMsg)
/* reparse custom tracks from URLs based on cart variables */
{
struct customTrack *ct;
struct customTrack *replacedCts = NULL;
struct customTrack *refreshCts = NULL;

for (ct = ctList; ct != NULL; ct = ct->next)
    {
    char var[256];
    safef(var, sizeof var, "%s_%s", hgCtRefreshPrefix, ct->tdb->track);
    if (cartUsualBoolean(cart, var, FALSE))
	{
	struct customTrack *nextCt = NULL, *urlCt = NULL;
	struct customTrack *urlCts =
	    customFactoryParse(database, ctDataUrl(ct), FALSE, NULL);
	for (urlCt = urlCts; urlCt != NULL; urlCt = nextCt)
	    {
	    nextCt = urlCt->next;
	    if (sameString(ct->tdb->track, urlCt->tdb->track))
		slAddHead(&refreshCts, urlCt);
	    }
	}
    }
ctList = customTrackAddToList(ctList, refreshCts, &replacedCts, FALSE);
if (warnMsg)
    *warnMsg = replacedTracksMsg(replacedCts);
customTrackHandleLift(database, ctList);
}

void addWarning(struct dyString *ds, char *msg)
/* build up a warning message from parts */
{
if (!msg)
    return;
if (isNotEmpty(ds->string))
    dyStringAppend(ds, ". ");
dyStringAppend(ds, msg);
}

char *saveLines(char *text, int max)
/* save lines from input, up to 'max'.
 * Prepend with comment, if truncated */
{
if (!text)
    return NULL;

char buf[128];
int count = 0;
char *line;
boolean truncated = FALSE;
struct dyString *ds = dyStringNew(0);

safef(buf, sizeof buf, "# Displaying first %d lines of data", max);
struct lineFile *lf = lineFileOnString("saved custom text", TRUE, text);
while (lineFileNext(lf, &line, NULL))
    {
    if (startsWith(buf, line))
        continue;
    if (++count > max)
        {
        truncated = TRUE;
        break;
        }
    dyStringAppend(ds, line);
    dyStringAppend(ds, "\n");
    }
if (truncated)
    {
    struct dyString *dsNew = dyStringNew(0);
    dyStringPrintf(dsNew, "%s\n%s", buf, dyStringCannibalize(&ds));
    return dyStringCannibalize(&dsNew);
    }
return (dyStringCannibalize(&ds));
}

struct customTrack *ctFromList(struct customTrack *ctList, char *track)
/* return custom track from list */
{
struct customTrack *ct = NULL;
for (ct = ctList; ct != NULL; ct = ct->next)
    if (sameString(track, ct->tdb->track))
        return ct;
return NULL;
}

boolean customTrackHasConfig(char *text)
/* determine if there are track or browser lines in text */
{
text = skipLeadingSpaces(text);
return startsWith("track ", text) || startsWith("browser ", text);
}

int timerCounter;
#define TIMER_INTERVAL 10

static void timer(int sig)
{
// Per HTML 4.01 spec (http://www.w3.org/TR/html401/struct/global.html#h-7.1):
//
//      White space (spaces, newlines, tabs, and comments) may appear before or after each section [including the DOCTYPE].
//
// So we print out comments periodically to keep this process from being killed by apache or the user's web browser.

printf("<!-- processing (%d seconds) -->\n", timerCounter++ * TIMER_INTERVAL);
fflush(stdout);
alarm(TIMER_INTERVAL);
}

void doMiddle(struct cart *theCart)
/* create web page */
{
char *ctFileName = NULL;
struct slName *browserLines = NULL;
struct customTrack *replacedCts = NULL;
char *err = NULL, *warnMsg = NULL;
char *selectedTable = NULL;
struct customTrack *ct = NULL;
boolean ctUpdated = FALSE;
char *initialDb = NULL;

long thisTime = clock1000();

cart = theCart;
measureTiming = isNotEmpty(cartOptionalString(cart, "measureTiming"));
initialDb = cloneString(cartUsualString(cart, "db", ""));
getDbAndGenome(cart, &database, &organism, oldVars);
chromAliasSetup(database);

customFactoryEnableExtraChecking(TRUE);

knetUdcInstall();
if (udcCacheTimeout() < 300)
    udcSetCacheTimeout(300);

if (sameString(initialDb, "0"))
    {
    /* when an organism is selected from the custom track management page,
     * set the database to be the default only if it has custom tracks.
     * Otherwise, pick an assembly for that organism that does have custom tracks. */
    struct dbDb *dbDb, *dbDbs = getCustomTrackDatabases();
    char *dbWithCts = NULL;
    for (dbDb = dbDbs; dbDb != NULL; dbDb = dbDb->next)
        {
        if (sameString(database, dbDb->name))
            break;
        if (sameString(organism, dbDb->organism))
            {
            if (!dbWithCts)
                dbWithCts = cloneString(dbDb->name);
            }
        }
    if (dbWithCts)  // set the database for the selected organism to an assembly that
        {           // has custom tracks
        database = dbWithCts;
        cartSetString(cart, "db", database);
        }
    }

if (cartVarExists(cart, hgCtDoAdd))
    doAddCustom(NULL, FALSE);
#ifdef PROGRESS_METER
else if (cartVarExists(cart, hgCtDoProgress))
    {
    doProgress(NULL);
    }
#endif
else if (cartVarExists(cart, hgCtTable))
    {
    /* update track */
    /* need to clone the hgCtTable value, as the ParseCart will remove
       the variable */
    selectedTable = cloneString(cartString(cart, hgCtTable));
    if (isNotEmpty(selectedTable))
        {
        ctList = customTracksParseCart(database, cart, NULL, NULL);
        ct = ctFromList(ctList, selectedTable);
        }
    if (ct)
        doUpdateCustom(ct, NULL, FALSE);
    else
        doAddCustom(NULL, FALSE);
    }
else
    {
    /* get new and existing custom tracks from cart and decide what to do */

    // setup a timer to periodically print out something to stdout to make sure apache or the web browser doesn't time us out (see redmine #3002).
    // e.g. see http://stackoverflow.com/questions/5547166/how-to-avoid-cgi-timeout
    struct sigaction *act;
    AllocVar(act);
    act->sa_handler = timer;
    act->sa_flags = SA_RESTART;
    sigaction(SIGALRM, act, NULL);
    alarm(TIMER_INTERVAL);

    boolean warnOnly = FALSE;

    char *customText = fixNewData(cart);
    /* save input so we can display if there's an error */
    char *savedCustomText = saveLines(cloneString(customText),
                                SAVED_LINE_COUNT);
    char *trackConfig = cartOptionalString(cart, hgCtConfigLines);
    char *savedConfig = cloneString(trackConfig);

    struct dyString *dsWarn = dyStringNew(0);
    char *fileName = cartOptionalString(cart, hgCtDataFileName);
    boolean hasData = (isNotEmpty(customText) || isNotEmpty(fileName));
    if (cartVarExists(cart, hgCtUpdatedTrack) && hasData)
        {
        /* from 'update' screen */
        /* prepend config to data for parser */
        struct dyString *dsTrack = dyStringNew(0);
        if (!trackConfig)
            trackConfig = cartOptionalString(cart, hgCtUpdatedTrack);
        char *fileContents = NULL;
        if (isNotEmpty(fileName))
            {
            if (customTrackIsCompressed(fileName))
                fileContents = "Compressed files not supported for data update";
            else
                fileContents = cartOptionalString(cart, hgCtDataFile);
            customText = fileContents;
            }
        /* check for duplicate track config in config and data entry */
        if (customTrackHasConfig(trackConfig) &&
            customTrackHasConfig(customText))
            {
            if (startsWith(trackConfig, customText))
                trackConfig = "";
            else
                customText = "Duplicate track configuration found - remove track and browser lines from Configuration box or from Data";
            }
        dyStringPrintf(dsTrack, "%s\n%s\n", trackConfig, customText);
        customText = dyStringCannibalize(&dsTrack);
        cartSetString(cart, hgCtDataText, customText);
        if (isNotEmpty(fileContents))
            {
            /* already handled file */
            cartRemove(cart, hgCtDataFile);
            cartRemove(cart, hgCtDataFileName);
            }
        }
    warnOnly = FALSE;
    struct errCatch *catch = errCatchNew();
    if (errCatchStart(catch))
	ctList = customTracksParseCartDetailed(database, cart, &browserLines, &ctFileName,
					       &replacedCts, NULL, &err, &warnOnly);
    errCatchEnd(catch);
    if (catch->gotError)
	{
	addWarning(dsWarn, err);
	}
    if (isNotEmpty(catch->message->string))
	{
        addWarning(dsWarn, catch->message->string);
	}
    errCatchFree(&catch);

    /* exclude special setting used by table browser to indicate
     * db assembly for error-handling purposes only */
    char *db = NULL;
    if (trackConfig && (db = stringIn("db=", trackConfig)) != NULL)
        {
        db += 3;
        char *nextTok = skipToSpaces(db);
        if (!nextTok)
            nextTok = strchr(db, 0);
        db = cloneStringZ(db,nextTok-db);
        if (!sameString(db,database))
            err = "Invalid configuration found - remove db= or return it to its original value. ";
        }

    if (cartVarExists(cart, hgCtUpdatedTrack) && !hasData)
        {
        /* update custom track config and doc, but not data*/
        selectedTable = cartOptionalString(cart, hgCtUpdatedTable);
        if (selectedTable)
            {
            ct = ctFromList(ctList, selectedTable);
            if (ct)
                {
                struct errCatch *catch = errCatchNew();
                if (errCatchStart(catch))
                    {
                    customTrackUpdateFromConfig(ct, database, trackConfig, &browserLines);
                    ctUpdated = TRUE;
                    }
                errCatchEnd(catch);
                if (isNotEmpty(catch->message->string))
                    err = catTwoStrings(emptyForNull(err), catch->message->string);
                errCatchFree(&catch);
                }
            }
        }

    addWarning(dsWarn, replacedTracksMsg(replacedCts));
    doBrowserLines(browserLines, &warnMsg);
    addWarning(dsWarn, warnMsg);
    if (err && !(warnOnly && cartVarExists(cart, "ContinueWithWarn")))
	{
        char *selectedTable = NULL;
        cartSetString(cart, hgCtDataText, savedCustomText);
        cartSetString(cart, hgCtConfigLines, savedConfig);
        if ((selectedTable= cartOptionalString(cart, hgCtUpdatedTable)) != NULL)
            {
            ct = ctFromList(ctList, selectedTable);
            doUpdateCustom(ct, err, warnOnly);
            }
        else
            doAddCustom(err, warnOnly);
       	cartRemovePrefix(cart, hgCt);
	return;
	}
    if (cartVarExists(cart, hgCtDoDelete))
        {
	doDeleteCustom();
        ctUpdated = TRUE;
        }
    if (cartVarExists(cart, hgCtDoRefresh))
	{
	doRefreshCustom(&warnMsg);
	addWarning(dsWarn, warnMsg);
        ctUpdated = TRUE;
	}
    if (ctUpdated || ctConfigUpdate(ctFileName))
	{
	customTracksSaveCart(database, cart, ctList);

	/* refresh ctList again to pickup remote resource error state */
	warnOnly=FALSE;
	struct errCatch *catch = errCatchNew();
	if (errCatchStart(catch))
	    ctList = customTracksParseCartDetailed(database, cart, &browserLines, &ctFileName,
					       &replacedCts, NULL, &err, &warnOnly);
	errCatchEnd(catch);
	if (catch->gotError)
	    {
	    addWarning(dsWarn, err);
	    addWarning(dsWarn, catch->message->string);
	    }
	errCatchFree(&catch);
	}
    warnMsg = dyStringCannibalize(&dsWarn);
    if (measureTiming)
	{
	long lastTime = clock1000();
	loadTime = lastTime - thisTime;
	}


    if (ctList || cartVarExists(cart, hgCtDoDelete))
        doManageCustom(warnMsg);
    else
	doAddCustom(warnMsg, warnOnly);
    }
cartRemovePrefix(cart, hgCt);
cartRemove(cart, CT_CUSTOM_TEXT_VAR);
}


int main(int argc, char *argv[])
/* Process command line. */
{
long enteredMainTime = clock1000();
issueBotWarning = earlyBotCheck(enteredMainTime, "hgCustom", delayFraction, 0, 0, "html");
htmlPushEarlyHandlers();
oldVars = hashNew(10);
cgiSpoof(&argc, argv);
cartEmptyShell(doMiddle, hUserCookie(), excludeVars, oldVars);
cgiExitTime("hgCustom", enteredMainTime);
return 0;
}
