/**
 * Copyright (c) 2021 Dominic Masters
 * 
 * This software is released under the MIT License.
 * https://opensource.org/licenses/MIT
 */

#include "csv.h"

csvbufferresult_t csvBuffer(
  assetbuffer_t *asset, csvbuffercallback_t *callback, void *user
) {
  int32_t cellChar, read, i, currentColumnCount;
  char buffer[CSV_BUFFER_SIZE];
  char cell[CSV_CELL_SIZE_MAX];
  char c;
  bool callbackResponse;
  bool insideEncapsulation = false;
  csvbufferresult_t result = {
    .cellCount = 0,
    .rowCount = 0,
    .columnCount = 0
  };

  // Init the cell Char Index.
  cellChar = 0;
  currentColumnCount = 0;

  // Begin buffering.
  while(true) {
    // Read n bytes into our buffer
    read = assetBufferRead(asset, buffer, CSV_BUFFER_SIZE);

    // Now read back those bytes.
    for(i = 0; i < read; i++) {
      c = buffer[i];

      // Characters we flat out ignore
      if(c == '\r') continue;

      // Handle quote marks.
      if(c == '"') {
        if(buffer[i+1] == '"') {// "" means a single quote (double-escaped)
          i++;
          cell[cellChar++] = c;
        } else if(insideEncapsulation) {
          insideEncapsulation = false;
        } else {
          insideEncapsulation = true;
        }

        continue;
      }
      
      // Is the start of a new cell/row?
      if(c == '\0' || (!insideEncapsulation && (c == ',' || c == '\n'))) {
        cell[cellChar] = '\0';// Terminate Cell string

        // Fire off the callback
        if(callback != NULL) {
          callbackResponse = callback(asset, user, result.rowCount, currentColumnCount, cell);
          if(!callbackResponse) return result;
        }

        // Prepare for next row/cell
        currentColumnCount++;
        result.columnCount = mathMax(currentColumnCount, result.columnCount);
        if(c == '\n') {
          result.rowCount++;
          currentColumnCount = 0;
        }
        result.cellCount++;// Only count cells with
        cellChar = 0;
        continue;// Skip
      }

      // Add character to the cell.
      cell[cellChar++] = c;
    }

    if(read < CSV_BUFFER_SIZE) break;
  }

  // If this is an empty row we don't count it, otherwise we do.
  if(currentColumnCount != 0) result.rowCount++;

  return result;
}


bool _csvBufferRowParserCallback(
  assetbuffer_t *asset, void *user, int32_t row, int32_t column, char *data
) {
  csvbufferrowdata_t *rowData = (csvbufferrowdata_t *)user;

  // Now did we change rows?
  if(row != rowData->row) {
    // Yes we did, let's buffer the previous row.
    if(rowData->callback != NULL) {
      if(!rowData->callback(
        asset, rowData->user, rowData->row, &rowData->rowCurrent
      )) return false;
    }
    // Begin next row
    rowData->row = row;
    rowData->rowCurrent.columnCount = 0;
  }

  // Determine string info for the cell
  int32_t length = strlen(data);
  int32_t offset = (column * CSV_CELL_SIZE_MAX);

  // Now copy the string data to the buffer
  arrayCopy(sizeof(char), data, length + 1, rowData->rowCurrent.data + offset);
  // Update the pointer to the string
  rowData->rowCurrent.columns[column] = rowData->rowCurrent.data + offset;
  rowData->rowCurrent.columnCount++;
  return true;
}

csvbufferresult_t csvBufferRow(
  assetbuffer_t *asset, csvbufferrowcallback_t *callback, void *user
) {
  csvbufferrowdata_t data;
  csvbufferresult_t result;
  data.row = 0;
  data.user = user;
  data.callback = callback;
  data.rowCurrent.columnCount = 0;

  // Perform a per-cell buffer and run the parser callback.
  result = csvBuffer(asset, &_csvBufferRowParserCallback, &data);

  // Because the buffer may not fire for the last row we handle it here.
  if(data.rowCurrent.columnCount > 0 && callback != NULL) {
    if(!callback(asset, user, data.row, &data.rowCurrent)) return result;
  }

  return result;
}


bool _csvBufferRowWithHeadersCallback(
  assetbuffer_t *asset, void *user, int32_t row, csvrow_t *csv
) {
  csvbufferrowwithheadersdata_t *data = (csvbufferrowwithheadersdata_t *)user;
  int32_t i;

  // Take the headers for row 0
  if(row == 0) {
    csvRowPopulate(csv, &data->headerRow);
    return true;
  }

  // Fire the callback
  return data->callback(asset, data->user, row, &data->headerRow, csv);
}

csvbufferresult_t csvBufferRowWithHeaders(
  assetbuffer_t *asset, csvbufferrowwitheaderscallback_t *callback, void *user
) {
  csvbufferrowwithheadersdata_t data;
  data.user = user;
  data.callback = callback;

  return csvBufferRow(asset, &_csvBufferRowWithHeadersCallback, &data);
}

void csvRowPopulate(csvrow_t *source, csvrow_t *dest) {
  int32_t i;

  dest->columnCount = source->columnCount;

  // Copy the raw characters from the source buffer.
  arrayCopy(sizeof(char), source->data, CSV_ROW_CHARACTERS_MAX, dest->data);

  // Now update the destination pointers to reference the data buffer.
  for(i = 0; i < source->columnCount; i++) {
    dest->columns[i] = dest->data + (i * CSV_CELL_SIZE_MAX);
  }
}


bool _csvHeadersGetCallback(
  assetbuffer_t *asset, void *user, int32_t row, csvrow_t *current
) {
  csvrow_t *rowData = (csvrow_t *)user;
  csvRowPopulate(current, rowData);
  return false;// False to break the loop
}

csvbufferresult_t csvHeadersGet(assetbuffer_t *asset, csvrow_t *row) {
  return csvBufferRow(asset, &_csvHeadersGetCallback, row);
}

int32_t csvColumnGetIndex(csvrow_t *row, char *key) {
  return arrayFindString(row->columns, row->columnCount, key);
}


bool _csvRowSearchCallback(
  assetbuffer_t *asset, void *user, int32_t row, csvrow_t *csv
) {
  csvsearchdata_t *data = (csvsearchdata_t *)user;
  // Does the search match?
  if(strcmp(csv->columns[data->column], data->value) != 0) return true;
  // Matched, copy and end.
  csvRowPopulate(csv, data->row);
  data->rowIndex = row;
  return false;
}
int32_t csvRowSearch(
  assetbuffer_t *asset, csvrow_t *row, int32_t column, char *value
) {
  csvsearchdata_t data = {
    .column = column,
    .row = row,
    .rowIndex = -1,
    .value = value
  };
  csvBufferRow(asset, &_csvRowSearchCallback, &data);
  return data.rowIndex;
}