function WikiFormatter()
{
  /*
   * This is the entry point, it takes a chunk of text, splits it into lines, loops
   * through the lines collecting consecutive lines that are part of a table, and returns
   * a chunk of text with those tables it collected formatted.
   */
  this.format = function(wikiText) {
    this.wikificationPrevention = false;
    
    var formatted = "";
    var currentTable = [];
    var lines = wikiText.split("\n");
    var line = null;

    for(var i = 0, j = lines.length; i < j; i++) {
      line = lines[i];
      
      if(this.isTableRow(line)) {
        currentTable.push(line);
      }
      else {
        formatted += this.formatTable(currentTable);
        currentTable = [];        
        formatted += line + "\n";
      }
    }

    formatted += this.formatTable(currentTable);
    return formatted.slice(0, formatted.length - 1);
  }

  /*
   * This function receives an array of strings(rows), it splits each of those strings
   * into an array of strings(columns), calls off to calculate what the widths
   * of each of those columns should be and then returns a string with each column
   * right/space padded based on the calculated widths.
   */
  this.formatTable = function(table) {
    var formatted = "";
    var splitRowsResult = this.splitRows(table);
    var rows = splitRowsResult.rows;
    var suffixes = splitRowsResult.suffixes;
    var widths = this.calculateColumnWidths(rows);
    var row = null;
  
    for(var rowIndex = 0, numberOfRows = rows.length; rowIndex < numberOfRows; rowIndex++) {
      row = rows[rowIndex];
      formatted += "|";

      for(var columnIndex = 0, numberOfColumns = row.length; columnIndex < numberOfColumns; columnIndex++) {
        formatted += this.rightPad(row[columnIndex], widths[rowIndex][columnIndex]) + "|";
      }

      formatted += suffixes[rowIndex] + "\n";
    }

    if(this.wikificationPrevention) {
      formatted = '!|' + formatted.substr(2);
      this.wikificationPrevention = false;
    }

    return formatted;
  }

  /* 
   * This is where the nastiness starts due to trying to emulate
   * the html rendering of colspans.
   *   - make a row/column matrix that contains data lengths
   *   - find the max widths of those columns that don't have colspans
   *   - update the matrix to set each non colspan column to those max widths
   *   - find the max widths of the colspan columns
   *   - increase the non colspan columns if the colspan columns lengths are greater
   *   - adjust colspan columns to pad out to the max length of the row
   *
   * Feel free to refator as necessary for clarity
   */
  this.calculateColumnWidths = function(rows) {
    var widths = this.getRealColumnWidths(rows);
    var totalNumberOfColumns = this.getNumberOfColumns(rows);

    var maxWidths = this.getMaxWidths(widths, totalNumberOfColumns);    
    this.setMaxWidthsOnNonColspanColumns(widths, maxWidths);
    
    var colspanWidths = this.getColspanWidth(widths, totalNumberOfColumns);
    this.adjustWidthsForColspans(widths, maxWidths, colspanWidths);
    
    this.adjustColspansForWidths(widths, maxWidths);
    
    return widths;
  }

  this.isTableRow = function(line) {
    return line.match(/^!?\|/);
  }

  this.splitRows = function(rows) {
    var splitRows = [];
    var rowSuffixes = [];

    this.each(rows, function(row) {
      var columns = this.splitRow(row);
      rowSuffixes.push(columns[columns.length - 1]);
      splitRows.push(columns.slice(0, columns.length - 1));
    }, this);

    return {rows: splitRows, suffixes: rowSuffixes};
  }

  this.splitRow = function(row) {
    var columns = this.trim(row).split('|');

    if(!this.wikificationPrevention && columns[0] == '!') {
      this.wikificationPrevention = true;
      columns[1] = '!' + columns[1]; //leave a placeholder
    }

    columns = columns.slice(1, columns.length);

    this.each(columns, function(column, i) {
      columns[i] = this.trim(column);
    }, this);

    return columns;
  }
  
  this.getRealColumnWidths = function(rows) {
    var widths = [];

    this.each(rows, function(row, rowIndex) {
      widths.push([]);
      
      this.each(row, function(column, columnIndex) {
        widths[rowIndex][columnIndex] = column.length;
      }, this);
    }, this);

    return widths;
  }

  this.getMaxWidths = function(widths, totalNumberOfColumns) {
    var maxWidths = [];
    var row = null;
    
    this.each(widths, function(row, rowIndex) {
      this.each(row, function(columnWidth, columnIndex) {
        if(columnIndex == (row.length - 1) && row.length < totalNumberOfColumns) {
          return false;
        }
        
        if(columnIndex >= maxWidths.length) {
          maxWidths.push(columnWidth);
        }
        else if(columnWidth > maxWidths[columnIndex]) {
          maxWidths[columnIndex] = columnWidth;
        }        
      }, this);
    }, this);
    
    return maxWidths;
  }
  
  this.getNumberOfColumns = function(rows) {
    var numberOfColumns = 0;

    this.each(rows, function(row) {
      if(row.length > numberOfColumns) {
        numberOfColumns = row.length;
      }
    });

    return numberOfColumns;
  }
  
  this.getColspanWidth = function(widths, totalNumberOfColumns) {
    var colspanWidths = [];
    var colspan = null;
    var colspanWidth = null;

    this.each(widths, function(row, rowIndex) {
      if(row.length < totalNumberOfColumns) {
        colspan = totalNumberOfColumns - row.length;
        colspanWidth = row[row.length - 1];
        
        if(colspan >= colspanWidths.length) {
          colspanWidths[colspan] = colspanWidth;
        }
        else if(!colspanWidths[colspan] || colspanWidth > colspanWidths[colspan]) {
          colspanWidths[colspan] = colspanWidth;
        }
      }
    });
    
    return colspanWidths;
  }
  
  this.setMaxWidthsOnNonColspanColumns = function(widths, maxWidths) {
    this.each(widths, function(row, rowIndex) {
      this.each(row, function(columnWidth, columnIndex) {
        if(columnIndex == (row.length - 1) && row.length < maxWidths.length) {
          return false;
        }
                
        row[columnIndex] = maxWidths[columnIndex];
      }, this);
    }, this);
  }
  
  this.getWidthOfLastNumberOfColumns = function(maxWidths, numberOfColumns) {
    var width = 0;
    
    for(var i = 1; i <= numberOfColumns; i++) {
      width += maxWidths[maxWidths.length - i]
    }
    
    return width + numberOfColumns - 1; //add in length of separators
  }
  
  this.spreadOutExcessOverLastNumberOfColumns = function(maxWidths, excess, numberOfColumns){
    var columnToApplyExcessTo = maxWidths.length - numberOfColumns;
    
    for(var i = 0; i < excess; i++) {
      maxWidths[columnToApplyExcessTo++] += 1;
      
      if(columnToApplyExcessTo == maxWidths.length) {
        columnToApplyExcessTo = maxWidths.length - numberOfColumns;
      }
    }
  }
  
  this.adjustWidthsForColspans = function(widths, maxWidths, colspanWidths) {
    var lastNumberOfColumnsWidth = null;
    var excess = null;
    
    this.each(colspanWidths, function(colspanWidth, index) {
      lastNumberOfColumnsWidth = this.getWidthOfLastNumberOfColumns(maxWidths, index + 1);
      
      if(colspanWidth && colspanWidth > lastNumberOfColumnsWidth){
        excess = colspanWidth - lastNumberOfColumnsWidth;
        this.spreadOutExcessOverLastNumberOfColumns(maxWidths, excess, index + 1);
        this.setMaxWidthsOnNonColspanColumns(widths, maxWidths);
      }
    }, this);
  }
  
  this.adjustColspansForWidths = function(widths, maxWidths) {
    var colspan = null;
    var lastNumberOfColumnsWidth = null
    
    this.each(widths, function(row, rowIndex) {
      colspan = maxWidths.length - row.length + 1;
      
      if(colspan > 1) {
        row[row.length - 1] = this.getWidthOfLastNumberOfColumns(maxWidths, colspan);
      }      
    }, this);
  }

  /*
   * Utility functions
   */
  this.trim = function(text) {
    return (text || "").replace( /^\s+|\s+$/g, "" );
  }
  
  this.each = function(array, callback, context) {
    var index = 0;
    var length = array.length;

    while(index < length && callback.call(context, array[index], index) !== false) {
      index++;
    }
  },

  this.rightPad = function(value, length) {
    var padded = value;

    for(var i = 0, j = length - value.length; i < j; i++) {
      padded += " ";
    }

    return padded;
  }
  
}
