/*-
* Copyright (c) 2010, 2013 The NetBSD Foundation, Inc.
* All rights reserved.
*
* This code is derived from software contributed to The NetBSD Foundation
* by David A. Holland.
*
* 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 NETBSD FOUNDATION, INC. 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 FOUNDATION 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.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include "bool.h"
#include "array.h"
#include "mode.h"
#include "place.h"
#include "files.h"
#include "directive.h"

struct incdir {
       const char *name;
       bool issystem;
};

DECLARRAY(incdir, static UNUSED);
DEFARRAY(incdir, static);

static struct incdirarray quotepath, bracketpath;

////////////////////////////////////////////////////////////
// management

static
struct incdir *
incdir_create(const char *name, bool issystem)
{
       struct incdir *id;

       id = domalloc(sizeof(*id));
       id->name = name;
       id->issystem = issystem;
       return id;
}

static
void
incdir_destroy(struct incdir *id)
{
       dofree(id, sizeof(*id));
}

void
files_init(void)
{
       incdirarray_init(&quotepath);
       incdirarray_init(&bracketpath);
}

DESTROYALL_ARRAY(incdir, );

void
files_cleanup(void)
{
       incdirarray_destroyall(&quotepath);
       incdirarray_cleanup(&quotepath);
       incdirarray_destroyall(&bracketpath);
       incdirarray_cleanup(&bracketpath);
}

////////////////////////////////////////////////////////////
// path setup

void
files_addquotepath(const char *dir, bool issystem)
{
       struct incdir *id;

       id = incdir_create(dir, issystem);
       incdirarray_add(&quotepath, id, NULL);
}

void
files_addbracketpath(const char *dir, bool issystem)
{
       struct incdir *id;

       id = incdir_create(dir, issystem);
       incdirarray_add(&bracketpath, id, NULL);
}

////////////////////////////////////////////////////////////
// parsing

/*
* Find the end of the logical line. End of line characters that are
* commented out do not count.
*/
static
size_t
findeol(const char *buf, size_t start, size_t limit)
{
       size_t i;
       int incomment = 0;
       bool inquote = false;
       char quote = '\0';

       for (i=start; i<limit; i++) {
               if (incomment) {
                       if (i+1 < limit && buf[i] == '*' && buf[i+1] == '/') {
                               i++;
                               incomment = 0;
                       }
               } else if (!inquote && i+1 < limit &&
                          buf[i] == '/' && buf[i+1] == '*') {
                       i++;
                       incomment = 1;
               } else if (i+1 < limit &&
                          buf[i] == '\\' && buf[i+1] != '\n') {
                       i++;
               } else if (!inquote && (buf[i] == '"' || buf[i] == '\'')) {
                       inquote = true;
                       quote = buf[i];
               } else if (inquote && buf[i] == quote) {
                       inquote = false;
               } else if (buf[i] == '\n') {
                       return i;
               }
       }
       return limit;
}

static
unsigned
countnls(const char *buf, size_t start, size_t limit)
{
       size_t i;
       unsigned count = 0;

       for (i=start; i<limit; i++) {
               if (buf[i] == '\n') {
                       count++;
                       if (count == 0) {
                               /* just return the max and error downstream */
                               return count - 1;
                       }
               }
       }
       return count;
}

static
void
file_read(const struct placefile *pf, int fd, const char *name, bool toplevel)
{
       struct lineplace places;
       struct place ptmp;
       size_t bufend, bufmax, linestart, lineend, nextlinestart, tmp;
       ssize_t result;
       bool ateof = false;
       char *buf;

       place_setfilestart(&places.current, pf);
       places.nextline = places.current;

       if (name) {
               debuglog(&places.current, "Reading file %s", name);
       } else {
               debuglog(&places.current, "Reading standard input");
       }

       bufmax = 128;
       bufend = 0;
       linestart = 0;
       lineend = 0;
       buf = domalloc(bufmax);

       while (1) {
               if (lineend >= bufend) {
                       /* do not have a whole line in the buffer; read more */
                       assert(bufend >= linestart);
                       if (linestart > 0 && bufend > linestart) {
                               /* slide to beginning of buffer */
                               memmove(buf, buf+linestart, bufend-linestart);
                               bufend -= linestart;
                               lineend -= linestart;
                               linestart = 0;
                       }
                       if (bufend >= bufmax) {
                               /* need bigger buffer */
                               buf = dorealloc(buf, bufmax, bufmax*2);
                               bufmax = bufmax*2;
                               /* just in case someone's screwing around */
                               if (bufmax > 0xffffffff) {
                                       complain(&places.current,
                                                "Input line too long");
                                       die();
                               }
                       }

                       if (ateof) {
                               /* don't read again, in case it's a socket */
                               result = 0;
                       } else {
                               result = read(fd, buf+bufend, bufmax - bufend);
                       }

                       if (result == -1) {
                               /* read error */
                               complain(NULL, "%s: %s",
                                        name, strerror(errno));
                               complain_fail();
                       } else if (result == 0 && bufend == linestart) {
                               /* eof */
                               ateof = true;
                               break;
                       } else if (result == 0) {
                               /* eof in middle of line */
                               ateof = true;
                               ptmp = places.current;
                               place_addcolumns(&ptmp, bufend - linestart);
                               if (buf[bufend - 1] == '\n') {
                                       complain(&ptmp, "Unclosed comment");
                                       complain_fail();
                               } else {
                                       complain(&ptmp,
                                                "No newline at end of file");
                               }
                               if (mode.werror) {
                                       complain_fail();
                               }
                               assert(bufend < bufmax);
                               lineend = bufend++;
                               buf[lineend] = '\n';
                       } else {
                               bufend += (size_t)result;
                               lineend = findeol(buf, linestart, bufend);
                       }
                       /* loop in case we still don't have a whole line */
                       continue;
               }

               /* have a line */
               assert(buf[lineend] == '\n');
               buf[lineend] = '\0';
               nextlinestart = lineend+1;
               place_addlines(&places.nextline, 1);

               /* check for CR/NL */
               if (lineend > 0 && buf[lineend-1] == '\r') {
                       buf[lineend-1] = '\0';
                       lineend--;
               }

               /* check for continuation line */
               if (lineend > 0 && buf[lineend-1]=='\\') {
                       lineend--;
                       tmp = nextlinestart - lineend;
                       if (bufend > nextlinestart) {
                               memmove(buf+lineend, buf+nextlinestart,
                                       bufend - nextlinestart);
                       }
                       bufend -= tmp;
                       nextlinestart -= tmp;
                       lineend = findeol(buf, linestart, bufend);
                       /* might not have a whole line, so loop */
                       continue;
               }

               /* line now goes from linestart to lineend */
               assert(buf[lineend] == '\0');

               /* count how many commented-out newlines we swallowed */
               place_addlines(&places.nextline,
                              countnls(buf, linestart, lineend));

               /* process the line (even if it's empty) */
               directive_gotline(&places, buf+linestart, lineend-linestart);

               linestart = nextlinestart;
               lineend = findeol(buf, linestart, bufend);
               places.current = places.nextline;
       }

       if (toplevel) {
               directive_goteof(&places.current);
       }
       dofree(buf, bufmax);
}

////////////////////////////////////////////////////////////
// path search

static
char *
mkfilename(struct place *place, const char *dir, const char *file)
{
       size_t dlen, flen, rlen;
       char *ret;
       bool needslash = false;

       if (dir == NULL) {
               dir = place_getparsedir(place);
       }

       dlen = strlen(dir);
       flen = strlen(file);
       if (dlen > 0 && dir[dlen-1] != '/') {
               needslash = true;
       }

       rlen = dlen + (needslash ? 1 : 0) + flen;
       ret = domalloc(rlen + 1);
       strcpy(ret, dir);
       if (needslash) {
               strcat(ret, "/");
       }
       strcat(ret, file);
       return ret;
}

static
int
file_tryopen(const char *file)
{
       int fd;

       /* XXX check for non-regular files */

       fd = open(file, O_RDONLY);
       if (fd < 0) {
               if (errno != ENOENT && errno != ENOTDIR) {
                       complain(NULL, "%s: %s", file, strerror(errno));
               }
               return -1;
       }

       return fd;
}

static
void
file_search(struct place *place, struct incdirarray *path, const char *name)
{
       unsigned i, num;
       struct incdir *id;
       const struct placefile *pf;
       char *file;
       int fd;

       assert(place != NULL);

       if (name[0] == '/') {
               fd = file_tryopen(name);
               if (fd >= 0) {
                       pf = place_addfile(place, name, true);
                       file_read(pf, fd, name, false);
                       close(fd);
                       return;
               }
       } else {
               num = incdirarray_num(path);
               for (i=0; i<num; i++) {
                       id = incdirarray_get(path, i);
                       file = mkfilename(place, id->name, name);
                       fd = file_tryopen(file);
                       if (fd >= 0) {
                               pf = place_addfile(place, file, id->issystem);
                               file_read(pf, fd, file, false);
                               dostrfree(file);
                               close(fd);
                               return;
                       }
                       dostrfree(file);
               }
       }
       complain(place, "Include file %s not found", name);
       complain_fail();
}

void
file_readquote(struct place *place, const char *name)
{
       file_search(place, &quotepath, name);
}

void
file_readbracket(struct place *place, const char *name)
{
       file_search(place, &bracketpath, name);
}

void
file_readabsolute(struct place *place, const char *name)
{
       const struct placefile *pf;
       int fd;

       assert(place != NULL);

       if (name == NULL) {
               fd = STDIN_FILENO;
               pf = place_addfile(place, "<standard-input>", false);
       } else {
               fd = file_tryopen(name);
               if (fd < 0) {
                       complain(NULL, "%s: %s", name, strerror(errno));
                       die();
               }
               pf = place_addfile(place, name, false);
       }

       file_read(pf, fd, name, true);

       if (name != NULL) {
               close(fd);
       }
}