/*  Part of XPCE --- The SWI-Prolog GUI toolkit

    Author:        Jan Wielemaker and Anjo Anjewierden
    E-mail:        J.Wielemaker@vu.nl
    WWW:           http://www.swi-prolog.org/projects/xpce/
    Copyright (c)  2013, University of Amsterdam
                         VU University Amsterdam
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:

    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.

    2. Redistributions in binary form must reproduce the above copyright
       notice, this list of conditions and the following disclaimer in
       the documentation and/or other materials provided with the
       distribution.

    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
    FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
    COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
    INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
    BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
    LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
    ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    POSSIBILITY OF SUCH DAMAGE.
*/

/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SYNOPSIS:
	xpce-copy [-v] [-c] [-m mode] from to
	xpce-copy [-v] [-c] [-m mode] [-p] file file ... dir

Simple install replacement  to  be  used   for  XPCE  installation.  The
configure install.sh script is very slow,   while  all operating systems
appear to be using different versions of install. This should solve this
problem.

This installer is also used to install the system under windows to avoid
the limitiations and  portability  issues   around  the  Windows command
interpreter and copy command.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <string.h>

#ifndef MAXPATHLEN
#define MAXPATHLEN 1024
#endif

#ifndef TRUE
#define TRUE 1
#define FALSE 0
#endif

#define bool int
#define shift (argc--, argv++)

#define CPBUFSIZE 8192

#ifndef O_BINARY
#define O_BINARY 0
#endif

#if defined(_MSC_VER) || defined(__MINGW32__)
#ifndef __WINDOWS__
#define __WINDOWS__
#endif
#endif

#ifdef __WINDOWS__
#include <io.h>				/* read, write, etc */
#include <direct.h>			/* mkdir, chdir */
#define IsDirSep(c) ((c) == '/' || (c) == '\\')
#define DIRSEP '\\'
#define mode_t unsigned short
#else
#define IsDirSep(c) ((c) == '/')
#define DIRSEP '/'
#endif

		 /*******************************
		 *	       SETTINGS		*
		 *******************************/

unsigned short mode;
bool	 set_mode;
bool	 copy;
char *	 program;
int	 verbose=0;
bool	 makedirs;
bool	 strippath = -1;		/* i.e. use basename() */
bool	 installdirs = FALSE;		/* -d */
bool	 newer_only = FALSE;

static int
get_mode(const char *s, unsigned short *m)
{ unsigned short mode = 0;

  while(*s >= '0' && *s <= '7')
  { mode = (mode << 3) + *s++ - '0';
  }

  if ( *s )
    return FALSE;

  *m = mode;
  return TRUE;
}


static int
isdir(const char *path)
{ struct stat buf;
  char tmp[MAXPATHLEN];
  char *s;

  strcpy(tmp, path);			/* delete trailing / to work */
  s = tmp+strlen(tmp);			/* around bugs in some runtime */
  if ( s>tmp && s[-1] == '/' )		/* libraries */
    *--s = 0;

  if ( stat(tmp, &buf) < 0 ||
       (buf.st_mode & S_IFMT) != S_IFDIR )
    return FALSE;

  return TRUE;
}


static void
get_file_mode(const char *name, int fd, mode_t *m)
{ struct stat buf;

#ifdef HAVE_FSTAT
  if ( fstat(fd, &buf) == 0 )
#else
  if ( stat(name, &buf) == 0 )
#endif
    *m = buf.st_mode;
  else
    *m = 0755;				/* default (hack) */
}


static int
last_modified(const char *name, time_t *t)
{ struct stat buf;

  if ( stat(name, &buf) == 0 )
  { *t = buf.st_mtime;
    return 0;
  } else
    return -1;
}


const char *
basename(const char *path)
{ const char *base;

  for(base=path; *path; path++)
  { if ( IsDirSep(*path) )
      base = path+1;
  }

  return base;
}


char *
dirname(const char *path)		/* returns malloced directory name */
{ const char *base = basename(path);
  char *rval;

  while(base > path && IsDirSep(base[-1]) )
    base--;

  if ( !(rval = malloc(base-path+1)) )
  { perror("malloc");
    exit(1);
  }
  memcpy(rval, path, base-path);
  rval[base-path] = '\0';

  return rval;
}


static int
makedir(char *path)
{ again:

#ifdef __WINDOWS__
  if ( mkdir(path) == 0 )
#else
  if ( mkdir(path, 0777) == 0 )
#endif
  { free(path);
    return TRUE;
  }

  if ( errno == ENOENT )
  { if ( makedir(dirname(path)) )
      goto again;
  }

  free(path);
  return FALSE;
}


static char *
str_store(const char *in)
{ char *rval;

  if ( (rval = malloc(strlen(in)+1)) )
  { strcpy(rval, in);
    return rval;
  }

  perror("malloc");
  exit(1);
}


static int
install_dir(const char *name)
{ if ( !isdir(name) )
    return makedir(str_store(name));

  return TRUE;
}


static int
install_file(const char *from, const char *to)
{ int fdfrom, fdto;
  char buf[CPBUFSIZE];
  int rval;
  int n;
  mode_t m;

  if ( newer_only )
  { time_t to_time;

    if ( last_modified(to, &to_time) == 0 )
    { time_t from_time;

      if ( last_modified(from, &from_time) != 0 )
      { perror(from);
	return FALSE;
      }

      if ( difftime(from_time, to_time) < 0.0 )
      { if ( verbose >= 2 )
	  fprintf(stderr, "Skipped %s (not modified)\n", to);
	return TRUE;
      }
    }
  }

  if ( (fdfrom = open(from, O_RDONLY|O_BINARY)) < 0 )
  { perror(from);
    return FALSE;
  }

  if ( set_mode )
    m = mode;
  else
    get_file_mode(from, fdfrom, &m);

  unlink(to);
  again:
  if ( (fdto = open(to, O_WRONLY|O_BINARY|O_CREAT|O_TRUNC, m)) < 0 )
  { if ( errno == ENOENT && makedir(dirname(to)) )
      goto again;
    perror(to);
    close(fdfrom);
    return FALSE;
  }

  while( (n = read(fdfrom, buf, CPBUFSIZE)) > 0 )
  { char *b = buf;

    while(n > 0)
    { int n2;

      if ( (n2=write(fdto, b, n)) < 0 )
      { perror(to);
	rval = FALSE;
	goto out;
      }
      b += n2;
      n -= n2;
    }
  }
  if ( n < 0 )
  { perror(from);
    rval = FALSE;
  } else
    rval = TRUE;

out:
  close(fdfrom);
  close(fdto);

  return rval;
}


const char *
strip_path(const char *path, int strip)
{ if ( strip < 0 )
    return basename(path);
  else
  { const char *base = path;

    while(strip-- > 0)
    { while( IsDirSep(*base) )
	base++;
      while( *base && !IsDirSep(*base) )
	base++;
      while( IsDirSep(*base) )
	base++;
    }

    return base;
  }
}


static int
install_file_in_dir(const char *file, const char *dir)
{ char path[MAXPATHLEN];
  const char *base = strip_path(file, strippath);
  int rval;

  sprintf(path, "%s%c%s", dir, DIRSEP, base);

  if ( (rval = install_file(file, path)) && verbose )
    printf("%s\n", base);

  return rval;
}

		 /*******************************
		 *	       IGNORE		*
		 *******************************/

#define MAXLINE 1024
#define EOS '\0'

int compilePattern(const char *p);
int matchPattern(const char *s);

typedef struct icell *Icell;

struct icell
{ char *pattern;
  Icell next;
};

typedef struct
{ Icell head;
  Icell tail;
} ilist, *Ilist;

ilist _ignore_patterns;
Ilist ignore_patterns = &_ignore_patterns;

void
to_ignore_list(Ilist ign, const char *pattern)
{ char *p = str_store(pattern);
  Icell c = malloc(sizeof(struct icell));

  c->pattern = p;
  c->next = NULL;
  if ( ign->tail )
  { ign->tail->next = c;
    ign->tail = c;
  } else
  { ign->head = ign->tail = c;
  }
}


int
ignore(Ilist ign, const char *name)
{ Icell c;

  for(c = ignore_patterns->head; c; c=c->next)
  { compilePattern(c->pattern);
    if ( matchPattern(name) )
      return TRUE;
  }
  for(c = ign->head; c; c=c->next)
  { compilePattern(c->pattern);
     if ( matchPattern(name) )
       return TRUE;
  }

  return FALSE;
}


static void
ignoreForDir(Ilist l, char *d)
{ char n[MAXPATHLEN];
  FILE *fd;

  sprintf(n, "%s%c.cvsignore", d, DIRSEP);
  if ( (fd = fopen(n, "r")) )
  { char p[MAXLINE];

    while( fgets(p, sizeof(p), fd) )
    { char *s = p;
      int len;

      while(*s && *s < ' ')
	s++;
      len = strlen(s);

      while( len > 0 && s[len-1] < ' ' )
	len--;
      s[len] = EOS;

      if ( len > 0 )
      { if ( verbose )
	  printf("ignore %s in %s\n", s, d);
	to_ignore_list(l, s);
      }
    }

    fclose(fd);
  }
}


		 /*******************************
		 *	  WILDCHART MATCH	*
		 *******************************/

#define succeed return(TRUE);
#define fail	return(FALSE);
#ifndef uchar
#define uchar unsigned char
#endif

#define MAXCODE 512

#define ANY	128
#define STAR	129
#define ALT	130
#define JMP	131
#define ANYOF	132
#define EXIT	133

#define NOCURL	0
#define CURL	1

#define warning(text) \
	(fprintf(stderr, "%s: bad pattern: %s\n", program, text), \
	 exit(0))

#define Output(c)	{ if ( Out->size > MAXCODE-1 ) \
			  { warning("pattern too large"); \
			    return (char *) NULL; \
			  } \
			  Out->code[Out->size++] = c; \
			}
#define setMap(c)	{ map[(c)/8] |= 1 << ((c) % 8); }

static struct _pattern_buffer
{ int	size;
  uchar	code[MAXCODE];
} default_pattern_buffer;

static char	*compile_pattern(struct _pattern_buffer*, const char *, int);
static int	match_pattern(uchar *, const char *);

int
compilePattern(const char *p)
{ default_pattern_buffer.size = 0;
  if ( compile_pattern(&default_pattern_buffer, p, NOCURL) == (char *) NULL )
    fail;

  succeed;
}


static char *
compile_pattern(struct _pattern_buffer *Out, const char *p, int curl)
{ char c;

  for(;;)
  { switch(c = *p++)
    { case EOS:
	break;
      case '\\':
	Output(*p == EOS ? '\\' : (*p & 0x7f));
	if (*p == EOS )
	  break;
	p++;
	continue;
      case '?':
	Output(ANY);
	continue;
      case '*':
	Output(STAR);
	continue;
      case '[':
	{ uchar *map;
	  int n;

	  Output(ANYOF);
	  map = &Out->code[Out->size];
	  Out->size += 16;
	  if ( Out->size >= MAXCODE )
	  { warning("Pattern too long");
	    return (char *) NULL;
	  }

	  for( n=0; n < 16; n++)
	    map[n] = 0;

	  for(;;)
	  { switch( c = *p++ )
	    { case '\\':
		if ( *p == EOS )
		{ warning("Unmatched '['");
		  return (char *)NULL;
		}
		setMap(*p);
		p++;
		continue;
	      case ']':
		break;
	      default:
		if ( p[-1] != ']' && p[0] == '-' && p[1] != ']' )
		{ register int chr;

		  for ( chr=p[-1]; chr <= p[1]; chr++ )
		    setMap(chr);
		  p += 2;
		} else
		  setMap(c);
		continue;
	    }
	    break;
	  }

	  continue;
	}
      case '{':
	{ int ai, aj = -1;

	  for(;;)
	  { Output(ALT); ai = Out->size; Output(0);
	    if ( (p = compile_pattern(Out, p, CURL)) == (char *) NULL )
	      return (char *) NULL;
	    if ( aj > 0 )
	      Out->code[aj] = Out->size - aj;
	    if ( *p == ',' )
	    { Output(JMP); aj = Out->size; Output(0);
	      Out->code[ai] = Out->size - ai;
	      Output(ALT); ai = Out->size; Output(0);
	      p++;
	    } else if ( *p == '}' )
	    { p++;
	      break;
	    } else
	    { warning("Unmatched '{'");
	      return (char *) NULL;
	    }
	  }

	  continue;
	}
      case '}':
      case ',':
	if ( curl == CURL )
	{ p--;
	  return (char *)p;
	}
	/*FALLTHROUGH*/
      default:
	Output(c & 0x7f);
	continue;
    }

    Output(EXIT);
    return (char *)p;
  }
}


int
matchPattern(const char *s)
{ return match_pattern(default_pattern_buffer.code, s);
}


static int
match_pattern(uchar *p, const char *s)
{ uchar c;

  for(;;)
  { switch( c = *p++ )
    { case EXIT:
	  return (*s == EOS ? TRUE : FALSE);
      case ANY:						/* ? */
	  if ( *s == EOS )
	    fail;
	  s++;
	  continue;
      case ANYOF:					/* [...] */
	  if ( p[*s / 8] & (1 << (*s % 8)) )
	  { p += 16;
	    s++;
	    continue;
	  }
	  fail;
      case STAR:					/* * */
	  do
	  { if ( match_pattern(p, s) )
	      succeed;
	  } while( *s++ );
	  fail;
      case JMP:						/* { ... } */
	  p += *p;
	  continue;
      case ALT:
	  if ( match_pattern(p+1, s) )
	    succeed;
	  p += *p;
	  continue;
      default:						/* character */
	  if ( c != (uchar) *s )
	    fail;
	  s++;
	  continue;
    }
  }
}

		 /*******************************
		 *		MAIN		*
		 *******************************/

void
usage()
{ fprintf(stderr, "  Usage: %s options file ... directory\n", program);
  fprintf(stderr, "     or: %s options from to\n", program);
  fprintf(stderr, "     or: %s [-C dir] -d dir ...\n", program);
  fprintf(stderr, "options: [-v[N]] [-n] [-c] [-p[N]] [-C dir] [-m mode]\n");
  exit(1);
}


int
main(int argc, char **argv)
{ char *out;
  int errors = 0;

  program = argv[0];
  shift;
  while(argc > 0 && argv[0][0] == '-')
  { char *opts = &argv[0][1];

    shift;
    for( ; *opts; opts++ )
    { switch( *opts )
      { case 'c':
	  copy = TRUE;
	  break;
	case 'p':
	  strippath = 0;
	  makedirs = TRUE;
	  if ( isdigit(opts[1]&0xff) )
	  { opts++;
	    strippath = *opts - '0';
	  }
	  break;
	case 'd':
	  installdirs = TRUE;
	  break;
	case 'n':
	  newer_only = TRUE;
	  break;
	case 'v':
	  verbose++;
	  if ( isdigit(opts[1]&0xff) )
	  { opts++;
	    verbose = *opts - '0';
	  }
	  break;
	case 'C':
	{ char *dir = argv[0];

	  shift;
	  if ( chdir(dir) != 0 )
	  { perror(dir);
	    exit(1);
	  }
	  break;
	}
	case 'm':
	  if ( argc > 0 && get_mode(argv[0], &mode) )
	  { shift;
	    set_mode = TRUE;
	  }
	  break;
	default:
	  usage();
      }
    }
  }

  if ( argc == 0 )
    usage();
  out = argv[argc-1];

  if ( installdirs )
  { int i;

    for(i=0; i<argc; i++)
    { if ( !install_dir(argv[i]) )
	errors++;
    }
  } else
  { if ( isdir(out) )
    { int i;

      to_ignore_list(ignore_patterns, "*~");
      to_ignore_list(ignore_patterns, "*.bak");
      to_ignore_list(ignore_patterns, "*.old");
      to_ignore_list(ignore_patterns, ".cvsignore");
      to_ignore_list(ignore_patterns, "CVS");
      ignoreForDir(ignore_patterns, out);

      for(i=0; i<argc-1; i++)
      { if ( ignore(ignore_patterns, argv[i]) )
	  continue;

	if ( isdir(argv[i]) )
	{ if ( verbose )
	    fprintf(stderr, "Skipping directory %s\n", argv[i]);
	  continue;
	}

	if ( !install_file_in_dir(argv[i], out) )
	  errors++;
      }
    } else
    { if ( !install_file(argv[0], out) )
	errors++;
    }
  }

  return errors ? 1 : 0;
}
