/*
    instrument.c - file and directory instrumentation routines
    Copyright (C) 2007, 2008, 2009, 2010 siliconforks.com

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include <config.h>

#include "instrument.h"

#include <assert.h>
#include <errno.h>
#include <string.h>

#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "encoding.h"
#include "global.h"
#include "instrument-js.h"
#include "resource-manager.h"
#include "util.h"

static int g_verbose = 0;

static int string_ends_with(const char * s, const char * suffix) {
  size_t length = strlen(s);
  size_t suffix_length = strlen(suffix);
  if (length < suffix_length) {
    return 0;
  }
  return strcasecmp(s + (length - suffix_length), suffix) == 0;
}

static enum FileType get_file_type(const char * file) {
  if (string_ends_with(file, ".js")) {
    return FILE_TYPE_JS;
  }
  else if (string_ends_with(file, ".html") || string_ends_with(file, ".htm")) {
    return FILE_TYPE_HTML;
  }
  else {
    return FILE_TYPE_OTHER;
  }
}

static void check_same_file(const char * file1, const char * file2) {
  if (is_same_file(file1, file2)) {
    fatal("source and destination are the same");
  }
}

static void check_contains_file(const char * file1, const char * file2) {
  if (contains_file(file1, file2)) {
    fatal("%s contains %s", file1, file2);
  }
}

static void instrument_file(const char * source_file, const char * destination_file, const char * id, int instrumenting) {
  /* check if they are the same */
  char * canonical_source_file = make_canonical_path(source_file);
  char * canonical_destination_file = make_canonical_path(destination_file);
  check_same_file(canonical_source_file, canonical_destination_file);
  free(canonical_source_file);
  free(canonical_destination_file);

  if (instrumenting) {
    enum FileType file_type = get_file_type(source_file);
    switch (file_type) {
    case FILE_TYPE_OTHER:
    case FILE_TYPE_HTML:
      if (g_verbose) {
        printf("Copying file %s\n", id);
      }
      copy_file(source_file, destination_file);
      break;
    case FILE_TYPE_JS:
      {
        if (g_verbose) {
          printf("Instrumenting file %s\n", id);
        }

        FILE * input = xfopen(source_file, "rb");
        FILE * output = xfopen(destination_file, "wb");

        Stream * input_stream = Stream_new(0);
        Stream * output_stream = Stream_new(0);

        Stream_write_file_contents(input_stream, input);

        /*
        Check if the source file looks like an instrumented JavaScript file.
        */
        if (input_stream->length >= JSCOVERAGE_INSTRUMENTED_HEADER_LENGTH &&
            memcmp(input_stream->data, JSCOVERAGE_INSTRUMENTED_HEADER, JSCOVERAGE_INSTRUMENTED_HEADER_LENGTH) == 0) {
          fatal_command_line("file %s in the source directory appears to be already instrumented", id);
        }

        size_t num_characters = input_stream->length;
        uint16_t * characters = NULL;
        int result = jscoverage_bytes_to_characters(jscoverage_encoding, input_stream->data, input_stream->length, &characters, &num_characters);
        if (result == JSCOVERAGE_ERROR_ENCODING_NOT_SUPPORTED) {
          fatal("encoding %s not supported", jscoverage_encoding);
        }
        else if (result == JSCOVERAGE_ERROR_INVALID_BYTE_SEQUENCE) {
          fatal("error decoding %s in file %s", jscoverage_encoding, id);
        }
        jscoverage_instrument_js(id, characters, num_characters, output_stream);
        free(characters);

        if (fwrite(output_stream->data, 1, output_stream->length, output) != output_stream->length) {
          fatal("cannot write to file: %s", destination_file);
        }

        Stream_delete(input_stream);
        Stream_delete(output_stream);

        fclose(input);
        fclose(output);
      }
      break;
    }
  }
  else {
    if (g_verbose) {
      printf("Copying file %s (on --no-instrument list)\n", id);
    }

    copy_file(source_file, destination_file);
  }
}

void jscoverage_instrument(const char * source,
                           const char * destination,
                           int verbose,
                           char ** exclude,
                           int num_exclude,
                           char ** no_instrument,
                           int num_no_instrument)
{
  assert(source != NULL);
  assert(destination != NULL);

  g_verbose = verbose;

  /* check if they are the same */
  check_same_file(source, destination);

  /* check if source directory is an ancestor of destination directory */
  check_contains_file(source, destination);

  /* check that the source exists and is a directory */
  struct stat buf;
  xstat(source, &buf);
  if (! S_ISDIR(buf.st_mode)) {
    fatal("not a directory: %s", source);
  }

  /* if the destination directory exists, check that it is a jscoverage directory */
  if (stat(destination, &buf) == 0) {
    /* it exists */
    if (! S_ISDIR(buf.st_mode)) {
      fatal("not a directory: %s", destination);
    }
    if (! directory_is_empty(destination)) {
      char * expected_file = NULL;
      if (jscoverage_mode == JSCOVERAGE_MOZILLA) {
        char * modules_directory = make_path(destination, "modules");
        expected_file = make_path(modules_directory, "jscoverage.jsm");
        free(modules_directory);
      }
      else {
        expected_file = make_path(destination, "jscoverage.html");
      }
      if (stat(expected_file, &buf) == -1) {
        fatal("refusing to overwrite directory: %s", destination);
      }
      free(expected_file);
    }
  }
  else if (errno == ENOENT) {
    xmkdir(destination);
  }
  else {
    fatal("cannot stat directory: %s", destination);
  }

  /* copy the resources */
  if (jscoverage_mode == JSCOVERAGE_MOZILLA) {
    char * chrome_directory = make_path(destination, "chrome");
    char * jscoverage_chrome_directory = make_path(chrome_directory, "jscoverage");
    mkdirs(jscoverage_chrome_directory);
    copy_resource("jscoverage.manifest", chrome_directory);
    copy_resource("jscoverage.html", jscoverage_chrome_directory);
    copy_resource("jscoverage.css", jscoverage_chrome_directory);
    copy_resource("jscoverage.js", jscoverage_chrome_directory);
    copy_resource("jscoverage-throbber.gif", jscoverage_chrome_directory);
    copy_resource("jscoverage-highlight.css", jscoverage_chrome_directory);
    copy_resource("jscoverage.xul", jscoverage_chrome_directory);
    copy_resource("jscoverage-overlay.js", jscoverage_chrome_directory);
    free(jscoverage_chrome_directory);
    free(chrome_directory);

    char * modules_directory = make_path(destination, "modules");
    mkdirs(modules_directory);
    copy_resource("jscoverage.jsm", modules_directory);
    free(modules_directory);
  }
  else {
    jscoverage_copy_resources(destination);
  }

  /* finally: copy the directory */
  struct DirListEntry * list = make_recursive_dir_list(source);
  for (struct DirListEntry * p = list; p != NULL; p = p->next) {
    char * s = make_path(source, p->name);
    char * d = make_path(destination, p->name);

    /* check if it's on the exclude list */
    for (int i = 0; i < num_exclude; i++) {
      char * x = make_path(source, exclude[i]);
      if (is_same_file(x, s) || contains_file(x, s)) {
        free(x);
        goto cleanup;
      }
      free(x);
    }

    char * dd = make_dirname(d);
    mkdirs(dd);
    free(dd);

    int instrument_this = 1;

    /* check if it's on the no-instrument list */
    for (int i = 0; i < num_no_instrument; i++) {
      char * ni = make_path(source, no_instrument[i]);
      if (is_same_file(ni, s) || contains_file(ni, s)) {
        instrument_this = 0;
      }
      free(ni);
    }

    instrument_file(s, d, p->name, instrument_this);

  cleanup:
    free(s);
    free(d);
  }

  free_dir_list(list);
}
