#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <dir.h>
#include <sys\stat.h>

#define TAB_SIZE	 8
#define DEFAULT_MAX_DIFF 10

#define MAX_LINE_LEN	1023
#define END_OF_STRING	0

#define NO_DIFFERENCES	"No differences encountered\n"
#define TOO_MANY	"Too many differences encountered (Use /LBn to change the maximum)\n"
#define LINE_TOO_LONG	"FC: Line too long (exceeding %d chars)\n"

typedef enum { FALSE, TRUE } bool;

// Global Variables
bool NoTabExpand = FALSE, Trim = FALSE, FirstAndLastOnly = FALSE;
bool IgnoreCase = FALSE, ShowLineNumbers = FALSE;
FILE *FilePointers[2];
char *FileNames[2];
int MaxDifferences = DEFAULT_MAX_DIFF;
enum { DEFAULT, ASCII, BINARY } CompareMode = DEFAULT;

// Returns size of a file
long FileSize(FILE* Stream)
{
  long CurrPos, Length;

  CurrPos = ftell(Stream);
  fseek(Stream, 0L, SEEK_END);
  Length = ftell(Stream);
  fseek(Stream, CurrPos, SEEK_SET);
  return Length;
}

// displays help message
void HelpMessage()
{
  printf("Compares two files or sets of files and displays the differences between them.\n");
  printf("\nFC [/A] [/C] [/L] [/LBn] [/N] [/T] [/W] [/nnnn] [drive1:][path1]filename1");
  printf("\n   [drive2][path2]filename2");
  printf("\nFC /B [drive1][path1]filename1 [drive2:][path2]filename2\n");
  printf("\n /A    Displays only first and last lines for each set of differences.");
  printf("\n /B    Performs a binary comparison.");
  printf("\n /C    Disregards the case of letters.");
  printf("\n /L    Compares files as ASCII text.");
  printf("\n /LBn  Sets the maximum consecutive mismatches to n lines.");
  printf("\n /N    Displays the line numbers on an ASCII comparison.");
  printf("\n /T    Does not expand tabs to spaces.");
  printf("\n /W    Compress white space (tabs and spaces) for comparison.\n");
}

// Scan arguments for switches and files
bool ScanArguments(int argc, char *argv[])
{
  int FileCounter = 0;

  // scan through arguments
  for (int i = 1; i < argc; i++)
    if ((argv[i][0] == '/') || (argv[i][0] == '-')) // Switch char
      if (argv[i][2] == END_OF_STRING)
	switch (toupper(argv[i][1])) // Single char switch in the form "/x"
	{
	  case 'B':
	    CompareMode = BINARY;
	    break;
	  case 'L':
	    CompareMode = ASCII;
	    break;
	  case 'A':
	    FirstAndLastOnly = TRUE;
	    break;
	  case 'C':
	    IgnoreCase = TRUE;
	    break;
	  case 'N':
	    ShowLineNumbers = TRUE;
	    break;
	  case 'T':
	    NoTabExpand = TRUE;
	    break;
	  case 'W':
	    Trim = TRUE;
	    break;
	  case '?':
	  case 'H':
	    HelpMessage();
	    return FALSE;
	  default:
	    printf("FC: Invalid switch: %s\n", argv[i]);
	    return FALSE;
	}
      else	// Switch not single char
	if ((toupper(argv[i][1]) == 'L') && (toupper(argv[i][2]) == 'B'))
	  MaxDifferences = atoi(&argv[i][3]);
	else
	{
	  printf("FC: Invalid switch: %s\n", argv[i]);
	  return FALSE;
	}
    else	// Not a switch: it's a filename
    {
      if (FileCounter >= 2)
      {
	printf("FC: Too many filespecs\n");
	return FALSE;
      }
      FileNames[FileCounter] = strdup(argv[i]);
      FileCounter++;
    }

  switch (FileCounter)
  {
    case 0:
      printf("FC: No file specified\n");
      return FALSE;
    case 1:
      FileNames[1] = strdup(".");
  }
  return TRUE;
}

// Binary compare two files
void BinaryCompare(void)
{
  unsigned char a, b;
  long int i, FileSize1, FileSize2;
  int Differences = 0;

  FileSize1 = FileSize(FilePointers[0]);
  FileSize2 = FileSize(FilePointers[1]);
  if (FileSize1 != FileSize2)
  {
    printf("FC: Warning: the files are of different size!\n");
    if (FileSize1 < FileSize2) FileSize1 = FileSize2;
  }
  for (i = 0; i < FileSize1; i++)
  {
    fread(&a, 1, 1, FilePointers[0]);
    fread(&b, 1, 1, FilePointers[1]);
    if (a != b)
    {
      Differences++;
      if (Differences > MaxDifferences)
      {
	printf(TOO_MANY);
	return;
      }
      printf("%08X: %02X %02X\n", i, a, b);
    }
  }
  if (Differences == 0) printf(NO_DIFFERENCES);
}

// Reads a line from an ascii file
void ReadLine(FILE* fp, char* Line)
{
  char* CurrPos = Line;
  bool Space = FALSE;

  char c = ' ';
  while ((c != '\n') && (!feof(fp)))
  {
    c = fgetc(fp);
    if (Trim)
    {
      if ((c == ' ') || (c == '\t'))
      {
	if (Space) continue;	// No more blanks or tabs allowed: ignore
	Space = TRUE;
	c = ' ';                // Insert a blank
      }
      else
	Space = FALSE;
    }
    else
      if ((c == '\t') && (!NoTabExpand))
      {
	int LineLength = (int)(CurrPos - Line);
	int Blanks = TAB_SIZE - (LineLength % TAB_SIZE);
	if ((LineLength + Blanks) >= MAX_LINE_LEN)
	{
	  printf(LINE_TOO_LONG, MAX_LINE_LEN);
	  return;
	}
	do
	{
	  *CurrPos = ' '; CurrPos++;
	  Blanks--;
	}
	while (Blanks > 0);
	continue;		// Chars inserted: go on
      }

    if ((CurrPos - Line) < MAX_LINE_LEN)	// If there is room
    {
      if (c == END_OF_STRING)
      {
	printf("FC: Invalid char (#0) in file\n");
	return;
      }
      *CurrPos = c; CurrPos++;	// Insert the char
    }
    else
    {
      printf(LINE_TOO_LONG, MAX_LINE_LEN);
      return;
    }
  }
  *CurrPos = END_OF_STRING;	// Close the string
}

// Ascii compare two files
void AsciiCompare(void)
{
  unsigned int LineNumber;
  char* String[2];
  bool Equal, Show, FirstDiff;
  int Differences;
  int i;

  for (i = 0; i < 2; i++) String[i] = new char[MAX_LINE_LEN + 1];

  for (i = 0; i < 2; i++)
  {
    rewind(FilePointers[0]);
    rewind(FilePointers[1]);
    Differences = 0;
    LineNumber = 0; Show = TRUE; FirstDiff = TRUE;
    while (!(feof(FilePointers[0]) || feof(FilePointers[1])))
    {
      LineNumber++;
      ReadLine(FilePointers[0], String[0]);
      ReadLine(FilePointers[1], String[1]);
      if (!IgnoreCase)
	Equal = (bool)(strcmp(String[0], String[1]) == 0);
      else
	Equal = (bool)(strcmp(strupr(String[0]), strupr(String[1])) == 0);
      if (!Equal)
      {
	if (FirstDiff) printf("****** %s\n", FileNames[i]);
	FirstDiff = FALSE;
	Differences++;
	if (Differences > MaxDifferences)
	{
	  printf("******\n");
	  printf(TOO_MANY);
	  return;
	}
	if (Show)
	{
	  if (ShowLineNumbers) printf("%d: ", LineNumber);
	  printf("%s", String[i]);
	}
	if (FirstAndLastOnly) Show = FALSE;
      }
      else
      {
	Differences = 0;
	if (!Show)	// If it's the first matching line after a difference
	{
	  if (ShowLineNumbers) printf("%d: ", LineNumber);
	  printf("%s", String[i]);
	  Show = TRUE;
	}
      }
    }
    if (!Show)	// No matching line after a difference
    {		// Print the last one
      if (ShowLineNumbers) printf("%d: ", LineNumber);
      printf("%s", String[i]);
    }
    if (!FirstDiff) printf("******\n");
  }
  for (i = 0; i < 2; i++) delete (String[i]);
  if (Differences == 0) printf(NO_DIFFERENCES);
}

bool HasWildcards(const char* Filename)
{
  return (bool)(fnsplit(Filename, NULL, NULL, NULL, NULL) & WILDCARDS);
}

bool IsADirectory(char* Filename)
{
  struct stat statbuf;
  if (stat(Filename, &statbuf) != -1)
    if (statbuf.st_mode & S_IFDIR)
      return TRUE;
  return FALSE;
}

/* Add a '\' at the end of Filename if not already present */
void EndingBackSlash(char** Filename)
{
  if ((*Filename)[strlen(*Filename) - 1] != '\\')
  {
    char* Temp = (char*)malloc(strlen(*Filename) + 2);
    sprintf(Temp, "%s\\", *Filename);
    free(*Filename);
    (*Filename) = Temp;
  }
}

/* Extract the path part of the filename */
char* FilePath(char* Filename)
{
  char drive[MAXDRIVE];
  char dir[MAXDIR];
  char *Path;

  fnsplit(Filename, drive, dir, NULL, NULL);
  Path = (char*)malloc(strlen(drive) + strlen(dir) + 1);
  sprintf(Path, "%s%s", drive, dir);
  return Path;
}

bool BinaryFile(char* Filename)
{
  const char* BinExt[] = { ".EXE", ".COM", ".SYS", ".OBJ", ".LIB", ".BIN" };
  char Ext[MAXEXT];
  if (fnsplit(Filename, NULL, NULL, NULL, Ext) & EXTENSION)
    for (int i = 0; i < sizeof (BinExt)/sizeof (BinExt[0]); i++)
      if (strcmpi(BinExt[i], Ext) == 0) return TRUE;
  return FALSE;
}

int main(int argc, char *argv[])
{
  if (!ScanArguments(argc, argv)) return 1;

  char* Path0;
  char* Path1;
  if (IsADirectory(FileNames[0]))
  {
    Path0 = FileNames[0];
    EndingBackSlash(&Path0);
    /* Assume all files */
    FileNames[0] = (char*)malloc(strlen(Path0) + 3 + 1);
    sprintf(FileNames[0], "%s*.*", Path0);
  }
  else
    Path0 = FilePath(FileNames[0]);

  bool File1IsDirectory = TRUE;
  if (FileNames[1][strlen(FileNames[1]) - 1] == ':')	/* Disk drive only */
    Path1 = strdup(FileNames[1]);
  else if (IsADirectory(FileNames[1]))
  {
    Path1 = strdup(FileNames[1]);
    EndingBackSlash(&Path1);
  }
  else
  {
    Path1 = FilePath(FileNames[1]);
    /* Any wildcard here means "all files" */
    File1IsDirectory = HasWildcards(FileNames[1]);
  }

  int Count = 0;
  struct ffblk FindData;
  bool Done0 = (bool)findfirst(FileNames[0], &FindData, 0);
  while (!Done0)
  {
    free(FileNames[0]);
    FileNames[0] = (char*)malloc(strlen(Path0) + strlen(FindData.ff_name) + 1);
    sprintf(FileNames[0], "%s%s", Path0, FindData.ff_name);

    if (File1IsDirectory)
    {
      free(FileNames[1]);
      FileNames[1] = (char*)malloc(strlen(Path1) + strlen(FindData.ff_name) + 1);
      sprintf(FileNames[1], "%s%s", Path1, FindData.ff_name);
    }

    if ((FilePointers[1] = fopen(FileNames[1], "rb")) != NULL)
    {
      if ((FilePointers[0] = fopen(FileNames[0], "rb")) == NULL)
      {
	fcloseall();
	printf("FC: Error opening file %s\n", FileNames[0]);
	return 1;
      }
      printf("Comparing files %s and %s\n", FileNames[0], FileNames[1]);
      switch (CompareMode)
      {
	case DEFAULT:
	  if (BinaryFile(FileNames[0]) || BinaryFile(FileNames[1]))
	    BinaryCompare();
	  else
	    AsciiCompare();
	  break;
	case ASCII:
	  AsciiCompare();
	  break;
	case BINARY:
	  BinaryCompare();
	  break;
      }
      fcloseall();
    }
    Done0 = (bool)findnext(&FindData);
  }
  free (Path0);
  free (Path1);
  if (Count == 0)
  {
    fprintf(stderr, "FC: No such file or directory\n");
    return 1;
  }
  return 0;
}