/*
 * (c) 2004 Canonical Software Ltd.  All rights reserved.
 */


#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define STRNEQ(a,b) (strcmp(a,b)!=0)
#define STREQ(a,b) (strcmp(a,b)==0)
#define STREQN(a,b) (strncmp(a,b,strlen(b))==0)

#define CONFIG_FILE "/etc/gcc-opt.conf"
int debug=0;
#define DBG_ARGS	0x001
#define DBG_CMDS	0x002
#define DBG_PRINT_ARGS	0x002

extern char **environ;

struct options {
    char **CompilerFlags;		/* Flags were going to set if not specified */
    char **ForcedFlags;			/* Flags we force to be our way */
    char **ClearedFlags;		/* Flags the caller can't use (we clear them) */
    char **Command;			/* gcc-3.3.real for example, defaults to compile-time default */
    char **Prefix;			/* ccache for example, defaults to nothing */
    int  OptMin;			/* minimum optimization level */
    int  OptMax;			/* Max opt level */
    char **OptDefault;			/* If no -O given, use this. */
    char **Env;				/* changes to the environment */
};

void deleteArg(char **argv,int n)
{
    for (;argv[n]!=NULL; n++) {
	argv[n]=argv[n+1];
    }
    /* argv is now one pointer longer than it needs to be.  So what.
     * That'll get fixed at the next insert.  (Or execv() call.)
     */
}

/* Insert NewArgs into argv starting at position n */
char **insertArgs(char **argv,int n, char **NewArgs)
{
    int new;
    int old;
    char **ret;

    for (old=0;argv[old]!=NULL; old++) ;
    for (new=0;NewArgs[new]!=NULL; new++) ;

    ret=malloc((old+new+1)*sizeof(char*));
    if (n>0)
	memcpy(ret,argv,n*sizeof(char*));
    if (new)
	memcpy(&ret[n],NewArgs,new*sizeof(char*));
    if (n<old)
	memcpy(&ret[n+new],&argv[n],(old-n)*sizeof(char*));
    ret[old+new]=NULL;
    return ret;
}

char **insertName(char **argv,int n, char *Name, char *Package)
{
    char *NewArgs[3];
    NewArgs[0]=strdup(Name);
    NewArgs[1]=malloc(strlen(Package)+strlen(Name)+3);
    sprintf(NewArgs[1],"<%s>%s",Package,Name);
    NewArgs[2]=NULL;
    argv=insertArgs(argv,n,NewArgs);
    return argv;
}

/* Given an option string, split it into component fields, using space
 * and tab as delimiters
 */
char ** splitOptions(char *flagsIn)
{
    char *flags=strdup(flagsIn);
    char **ret=malloc(sizeof(char*));
    int  count=0;
    char *p;

    p=strtok(flags," \t");
    while (p!=NULL) {
	++count;
	ret=realloc(ret,(count+1)*sizeof(char*));
	ret[count-1]=p;
	p=strtok(NULL," \t");
    }
    ret[count]=NULL;
    return ret;
}

/* Read the config file to get options, and split them up.
 */
struct options *readOptions(char *name, char* config, char *Package)
{
    static struct options opt;
    FILE *f;
    char buf[2048];
    char *p;
    char **MyNames=calloc(1,sizeof(char*));
    char **namep;

    /* set up some defaults */
    memset(&opt,0,sizeof(opt));
    opt.OptMin=-1;

    p=strrchr(name,'/');
    if (p!=NULL) {
	MyNames=insertName(MyNames,0,name,Package);
	name=strdup(p+1);
    } else {
	name=strdup(name);
    }
    MyNames=insertName(MyNames,0,name,Package);
    if (strchr(name,'.')) {
	*strrchr(name,'.')='\0';
    }
    MyNames=insertName(MyNames,0,name,Package);
    p=strrchr(name,'-');
    if (p!=NULL) {
	*p='\0';
	MyNames=insertName(MyNames,0,name,Package);
    }
    while ((p=strchr(name,'-'))!=NULL) {
	p++;
	MyNames=insertName(MyNames,0,p,Package);
    }
    free(name); name=NULL;

    f=fopen(config,"r");
    if (f==NULL) {
	fprintf(stderr,"unable to open config file: %s\n",strerror(errno));
	exit(2);
    }
    while (fgets(buf,sizeof(buf),f)!=NULL) {
	int l=strlen(buf);
	if (buf[l-1]=='\n') {
	    buf[l-1]='\0';
	} else {
	    fprintf(stderr,"Line too long: %s\n",buf);
	    exit(2);
	}
	if ((p=strchr(buf,'#'))!=NULL) {
	    *p='\0';
	}
	for (namep=MyNames; *namep; namep++) {
	    int NameLength;
	    name=*namep;
	    NameLength=strlen(name);
	    if (strncmp(buf,name,NameLength)==0) {
		if (opt.CompilerFlags==NULL && buf[NameLength]==':') {
		    if (debug&DBG_ARGS)
			fprintf(stderr,"Picked %s\n",name);
		    opt.CompilerFlags=splitOptions(buf+NameLength+1);
		} else if (opt.ForcedFlags==NULL &&
			   strncmp(buf+NameLength,".force:",7)==0) {
		    if (debug&DBG_ARGS)
			fprintf(stderr,"Picked %s.force\n",name);
		    opt.ForcedFlags=splitOptions(strchr(buf,':')+1);
		} else if (opt.Command==NULL &&
			   strncmp(buf+NameLength,".command:",9)==0) {
		    if (debug&DBG_CMDS)
			fprintf(stderr,"Picked %s.command\n",name);
		    opt.Command=splitOptions(strchr(buf,':')+1);
		} else if (opt.Env==NULL &&
			   strncmp(buf+NameLength,".env:",5)==0) {
		    if (debug&DBG_CMDS)
			fprintf(stderr,"Picked %s.env\n",name);
		    opt.Env=splitOptions(strchr(buf,':')+1);
		} else if (opt.Prefix==NULL &&
			   strncmp(buf+NameLength,".prefix:",8)==0) {
		    if (debug&DBG_CMDS)
			fprintf(stderr,"Picked %s.prefix\n",name);
		    opt.Prefix=splitOptions(strchr(buf,':')+1);
		} else if (opt.ClearedFlags==NULL &&
			   strncmp(buf+NameLength,".clear:",7)==0) {
		    if (debug&DBG_ARGS)
			fprintf(stderr,"Picked %s.clear\n",name);
		    opt.ClearedFlags=splitOptions(strchr(buf,':')+1);
		} else if (opt.OptMin==-1 &&
			   strncmp(buf+NameLength,".opt:",5)==0) {
		    char **ap=splitOptions(strchr(buf,':')+1);
		    int i;

		    if (debug&DBG_ARGS)
			fprintf(stderr,"Picked %s.opt\n",name);
		    for (i=0;ap[i];i++);
		    opt.OptMax=3;
		    opt.OptMin=0;
		    switch (i) {
			case 2:
			    opt.OptMax=atol(ap[1]);
			    /* fall thru */
			case 1:
			    opt.OptMin=atol(ap[0]);
			    /* fall thru */
			case 0:
			    opt.OptDefault=ap+i;
			    break;
			default:
			    opt.OptMin=atol(ap[0]);
			    opt.OptMax=atol(ap[1]);
			    opt.OptDefault=ap+2;
			    break;
		    }
		}
	    }
	}
    }
    fclose(f);
    if (opt.CompilerFlags==NULL) {
	opt.CompilerFlags=calloc(1,sizeof(sizeof(opt.CompilerFlags[0])));
    }
    if (opt.ForcedFlags==NULL) {
	opt.ForcedFlags=calloc(1,sizeof(sizeof(opt.ForcedFlags[0])));
    }
    if (opt.ClearedFlags==NULL) {
	opt.ClearedFlags=calloc(1,sizeof(sizeof(opt.ClearedFlags[0])));
    }
    if (opt.OptDefault==NULL) {
	opt.OptMin=0;
	opt.OptMax=3;
	opt.OptDefault=calloc(1,sizeof(sizeof(opt.OptDefault[0])));
    }
    if (opt.Command==NULL) {
	char *ExecName;
	ExecName=getenv("__GCC_OPT_COMPILER_EXEC");
	if (ExecName==NULL)
	    if (strchr(name,'>')==NULL)
		ExecName=name;
	    else
		ExecName=strchr(name,'>')+1;
	opt.Command=splitOptions(ExecName);
    }
    if (opt.Prefix==NULL) {
	opt.Prefix=calloc(1,sizeof(sizeof(opt.Prefix[0])));
    }

    return &opt;
}

/* Return non-zero if the two flags are the same flag...  f1 is from
 * the config file, f2 is from the caller.
 */
int argMatch(const char *f1,const char *f2)
{
    const char *p1,*p2;

    if (f1[0]!=f2[0] || f1[1]!=f2[1])
	return 0;

    /* for -f args, strip off the '-f' and any 'no-' */
    if (f1[1]=='f') {
	f1+=2; if (f1[0]=='n' && f1[1]=='o' && f1[2]=='-') f1+=3;
	f2+=2; if (f2[0]=='n' && f2[1]=='o' && f2[2]=='-') f2+=3;
    }

    /* for -m args, deal with -m64 and -m32, which affect -march=...
     * and -mtune=...
     *
     * sigh. */
    if (f1[1]=='m') {
	if ((STREQ(f2,"-m64") || STREQ(f2,"-m32")) &&
	    (STREQN(f1,"-march=") || STREQN(f1,"-mtune=")))
		f1=f2;
	if ((STREQ(f1,"-m64") || STREQ(f1,"-m32")) &&
	    (STREQN(f2,"-march=") || STREQN(f2,"-mtune=")))
		f1=f2;
    }

    /* Are they args that take '='?  Ignore anything after the = */
    p1=strchr(f1,'='); p2=strchr(f2,'=');

    if ((p1==NULL) && (p2==NULL)) {
	return strcmp(f1,f2)==0;
    } else if ((p1 && p2 && (p1-f1 == p2-f2)) ||
               (p2 && (p1==NULL) && strlen(f1)==(p2-f2))) {
	return strncmp(f1,f2,(p2-f2))==0;
    } else {
	return 0;
    }
}

/* Return -1 if arg deleted from argv, 0 otherwise.
 */
int processOneArg(char **argv,int n,struct options *opt)
{
    int j;

    /* Do ClearedFlags first, so that we can clear specific options,
     * while having a default otherwise.
     */
    for (j=0;opt->ClearedFlags[j]!=NULL; j++) {
	char *p=opt->ClearedFlags[j];
	if (argMatch(p,argv[n])) {
	    fprintf(stderr,"gcc-opt: Clearing %s\n",argv[n]);
	    deleteArg(argv,n);
	    return -1;
	}
    }
    for (j=0;opt->CompilerFlags[j]!=NULL; j++) {
	char *p=opt->CompilerFlags[j];
	if (argMatch(p,argv[n])) {
	    deleteArg(opt->CompilerFlags,j);
	    j--;
	    continue;
	}
    }
    for (j=0;opt->ForcedFlags[j]!=NULL; j++) {
	char *p=opt->ForcedFlags[j];
	if (argMatch(p,argv[n])) {
	    if (STRNEQ(p,argv[n]))
		fprintf(stderr,"gcc-opt: Overriding %s with %s\n",argv[n],p);
	    deleteArg(argv,n);
	    return -1;
	}
    }
    return 0;
}

/* Walk through the user-supplied arguments, forcing things, clearing things
 * and so forth.
 */
char ** processArgs(int argc, char **argv, struct options *opt)
{
    char **MyArgv=malloc((argc+1)*sizeof(char*));
    int i,j;
    char *p;
    int optSpecified=0;

    memcpy(MyArgv,argv,argc*sizeof(char*));
    MyArgv[argc]=NULL;

    if (!(argc==2 && STREQ(MyArgv[1],"-v"))) {
	for (i=1; MyArgv[i]!=NULL; i++) {
	    if (MyArgv[i][0]=='-')
		if (MyArgv[i][1]=='O') {
		    char c=MyArgv[i][2];
		    int OptLevel;
		    if (c=='s')
			OptLevel=2;
		    else if (c=='\0')
			OptLevel=2;
		    else
			OptLevel=atoi(MyArgv[i]+2);
		    if (OptLevel>=opt->OptMin && OptLevel<=opt->OptMax) {
			optSpecified=1;
		    } else {
			if (OptLevel<opt->OptMin) {
			    char *s=malloc(10);
			    sprintf(s,"-O%d",opt->OptMin);
			    fprintf(stderr,"gcc-opt: forcing %s\n",s);
			    MyArgv[i]=s;
			} else {
			    char *s=malloc(10);
			    sprintf(s,"-O%d",opt->OptMax);
			    fprintf(stderr,"gcc-opt: forcing %s\n",s);
			    MyArgv[i]=s;
			}
		    }
		} else {
		    i+=processOneArg(MyArgv,i,opt);
		}
	}
	if (!optSpecified) {
	    MyArgv=insertArgs(MyArgv,1,opt->OptDefault);
	}
	MyArgv=insertArgs(MyArgv,1,opt->ForcedFlags);
	MyArgv=insertArgs(MyArgv,1,opt->CompilerFlags);
    }
    deleteArg(MyArgv,0);			/* Lose their command */
    MyArgv=insertArgs(MyArgv,0,opt->Command);	/* insert our command */
    MyArgv=insertArgs(MyArgv,0,opt->Prefix);	/* insert our prefix */
    return MyArgv;
}

int main(int argc, char *argv[])
{
    struct options *opt;

    char *ConfigFile;
    char *name;
    char *Package;
    char *p;
    char **e;

    if (p=getenv("__GCC_OPT_DEBUG")) {
	debug=strtol(p,NULL,0);
    }

    Package=getenv("__GCC_OPT_PACKAGE");
    if (Package==NULL) {
	FILE *f=fopen("/CurrentlyBuilding","r");
	if (f) {
	    static char buf[300];
	    static char buf2[sizeof(buf)];
	    Package=fgets(buf,sizeof(buf)-1,f);
	    if (Package && *Package) {
		if (Package[strlen(Package)-1]=='\n') {
		    Package[strlen(Package)-1]='\0';
		} else if (strlen(Package)==sizeof(buf)-2) {
		    fprintf(stderr,"gcc-opt: warning: Package name truncated\n");
		}
		if (sscanf(Package,"Package: %s",buf2) == 1) {
		    Package=buf2;
		}
	    } else {
	        fprintf(stderr,"gcc-opt: /CurrentlyBuilding is empty\n");
		Package="";
	    }
	    fclose(f);
	} else {
	    fprintf(stderr,"gcc-opt: Failed to open /CurrentlyBuilding\n");
	    Package="";
	}
    }
    name=getenv("__GCC_OPT_COMPILER_NAME");
    ConfigFile=getenv("__GCC_OPT_CONFIG_FILE");
    if (ConfigFile==NULL)
	ConfigFile=CONFIG_FILE;
    name=getenv("__GCC_OPT_COMPILER_NAME");
    if (name==NULL)
	name=COMPILER;

    opt=readOptions(name,ConfigFile,Package);
    argv=processArgs(argc,argv,opt);
    if (debug&DBG_PRINT_ARGS) {
	int i;
	for (i=0;argv[i];i++) {
	    fprintf(stderr,"%d: \"%s\"\n",i,argv[i]);
	}
    }
    if (opt->Env!=NULL) {
	for (e=opt->Env; e!=NULL; e++) {
	    if (**e=='-') {
		unsetenv(*e+1);
	    } else {
		p=strchr(*e,'=');
		if (p==NULL) {
		    fprintf(stderr,"gcc-opt: bad envvar: %s\n",*e);
		} else {
		    *p++='\0';
		    setenv(*e,p,1);
		}
	    }
	}
    }
    execv(argv[0],argv);
    perror("execv");
    exit(1);
}
